Skip to content

feat(example-app): (React) new GlassModal and Editor page upgrades (saving, histories, fullscreen, bug fixes) - #278

Merged
Ryan-Millard merged 40 commits into
mainfrom
feat/example-app/react/download-svg
Apr 4, 2026
Merged

feat(example-app): (React) new GlassModal and Editor page upgrades (saving, histories, fullscreen, bug fixes)#278
Ryan-Millard merged 40 commits into
mainfrom
feat/example-app/react/download-svg

Conversation

@Ryan-Millard

@Ryan-Millard Ryan-Millard commented Mar 11, 2026

Copy link
Copy Markdown
Owner

What was changed & why

New buttons to Editor page to make it more usable.

Fixes: #114
Fixes: #85
Fixes: #288

Changes

New GlassModal and HamburgerMenu. Navbar refactored, Editor page, too.

Testing & Verification

Additional Resources

Summary by CodeRabbit

  • New Features

    • Added modal component with backdrop, animations, and keyboard controls.
    • Introduced undo/redo history for editor changes with keyboard shortcuts (Ctrl/Cmd+Z, Ctrl/Cmd+Y).
    • Added fullscreen support to the editor.
    • Launched editor toolbar with actions for saving, downloading, printing, and sharing SVG files.
    • Implemented Escape key to close modals and reset confirmation dialog.
  • Style

    • Updated editor layout and component styling with responsive adjustments.

@github-actions github-actions Bot added the js/ts label Mar 11, 2026
@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements a modal component system (GlassModal), adds comprehensive export/share functionality via a new EditorControls component, and introduces history/undo-redo and fullscreen capabilities to the Editor. It refactors viewport transform handling to use refs and imperative updates, adds pointer event batching, integrates a useFullscreen hook, and updates CSS utilities for layout and styling.

Changes

Cohort / File(s) Summary
Modal Components
example-apps/react-js/src/components/GlassCard.jsx, example-apps/react-js/src/components/GlassModal.jsx, example-apps/react-js/src/components/GlassModal.module.css
Wrapped GlassCard with forwardRef to support ref forwarding. Added new GlassModal component with portal rendering, backdrop click/Escape key handling, animated entry, configurable size, optional close button, and fullscreen styling. Includes CSS with fade-in and scale animations.
Editor Controls
example-apps/react-js/src/pages/Editor/EditorControls.jsx, example-apps/react-js/src/pages/Editor/EditorControls.module.css
Introduced EditorControls component with Save modal, clipboard copy, download, print, and Web Share API integration. Includes color-mode toggle, history controls, and responsive toolbar layout with utility styling for action lists and screen-reader-only content.
Editor Core Logic
example-apps/react-js/src/pages/Editor/index.jsx, example-apps/react-js/src/pages/Editor/Editor.module.css
Refactored viewport transforms from React state to imperative ref-based system with requestAnimationFrame batching. Added history/undo/redo with snapshot recording on shape tap. Integrated useFullscreen hook, global keyboard shortcuts (Ctrl+Z/Cmd+Z for undo), pointer event listener for gesture batching, shape data-id initialization, and reset confirmation modal. Updated CSS for SVG element targeting, transform-origin, responsive layout, and new .historyContainer class.
Utilities & Hooks
example-apps/react-js/src/hooks/useFullscreen.js, example-apps/react-js/src/global-styles/base.css, example-apps/react-js/src/global-styles/utilities/layout.css, example-apps/react-js/src/components/GlassSwitch.module.css
Added useFullscreen hook with vendor-prefixed fullscreen API support, browser history integration, and Escape key handling. Extended base CSS for fullscreen backdrop styling and added button.anchor-style rule. Added .flex-wrap-wrap utility class. Removed right margin from .switch to support flexible control layout.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, ux

Suggested reviewers

  • Krasner

Poem

🐰✨ A modal springs forth with grace and light,
Controls to save and share your sight,
Undo, redo, fullscreen dreams—
The editor gleams with new extremes! 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed All coding requirements from #114 (GlassModal with open/close, overlay, Escape, aria-modal, close button, portal) and #85 (print, SVG/copy, share exports) are implemented and functional.
Out of Scope Changes check ✅ Passed All changes are directly related to linked issues: GlassModal/EditorControls for #114/#85, refactored Editor with history/fullscreen, and supporting CSS/component updates.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: new GlassModal component and Editor page upgrades including saving, histories, and fullscreen functionality.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/example-app/react/download-svg

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Nitpick comments (7)
example-apps/react-js/src/global-styles/components.css (1)

18-21: Stylelint: Expected empty line before declaration.

The color declaration at line 21 follows a nested ruleset without an empty line separator.

🔧 Proposed fix
   * {
     color: unset;
   }
+
   color: var(--color-text);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/global-styles/components.css` around lines 18 - 21,
The CSS has a missing blank line before the standalone "color:
var(--color-text);" declaration (it immediately follows the universal selector
block "* { color: unset; }"), so add a single empty line before the "color:
var(--color-text);" declaration to satisfy stylelint; locate the declaration by
searching for the "color: var(--color-text);" token or the universal selector
"*" and insert the empty line separator so declarations are properly separated.
example-apps/react-js/src/components/GlassModal.module.css (1)

23-33: Close button lacks explicit background color.

Without a background property, the button may inherit or default to unexpected styles depending on global resets. Consider adding background: transparent or the appropriate color.

💡 Proposed fix
 .closeButton {
   position: absolute;
   top: 0.5rem;
   right: 0.5rem;
   border: none;
+  background: transparent;
   cursor: pointer;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/components/GlassModal.module.css` around lines 23 -
33, The .closeButton CSS class lacks an explicit background which can lead to
inconsistent visuals; update the .closeButton rule to set an explicit background
(e.g., background: transparent or your design-system close button color) so it
doesn't inherit unexpected styles from global resets, and ensure the chosen
background works with existing padding/alignments in the .closeButton rule.
example-apps/react-js/src/components/HamburgerMenu.module.css (2)

41-45: The .open class outside the media query has no practical effect on desktop.

On desktop, .navList is display: flex and already visible. The .open class properties (opacity, visibility, transform) don't affect a visible flex container. Consider moving this rule inside the media query to clarify its mobile-only purpose.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/components/HamburgerMenu.module.css` around lines
41 - 45, The .open rule is currently defined outside the mobile media query but
has no effect on desktop because .navList is display:flex and already visible;
move the .open selector (the rule setting opacity, visibility, transform) into
the existing mobile-only media query alongside the .navList mobile rules so
.open only applies on small screens and controls the toggled mobile menu (refer
to the .open class and .navList selector to locate the relevant CSS).

99-111: Consider avoiding ID selectors in CSS modules.

Using #nav-menu creates implicit coupling between CSS and the hardcoded ID in the JSX. CSS modules work best when scoped through class selectors. If these styles need high specificity, consider using :where() with the module class or moving anchor-specific styles to the module class directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/components/HamburgerMenu.module.css` around lines
99 - 111, The styles use an ID selector ("#nav-menu a", "a:link", "a:visited")
which couples the CSS module to a hardcoded DOM id; replace the ID selector with
a module-scoped class (e.g., create a .navMenu or .navAnchor class in
HamburgerMenu.module.css and move the anchor rules there) or scope via
:where(.navModuleClass) to keep specificity while staying module-scoped, then
update the component JSX to apply that className to the nav container or anchors
instead of relying on the `#nav-menu` id.
example-apps/react-js/src/components/GlassCard.jsx (1)

6-10: Consider adding displayName for better debugging.

When using forwardRef, React DevTools will show the component as "Anonymous" unless you set displayName. This aids debugging.

💡 Proposed fix
 const GlassCard = forwardRef( ({ as: Tag = "div", children, ...rest }, ref) => (
   <Tag {...rest} className={`text-center glass ${styles.card} ${rest.className || ""}`} ref={ref}>
     {children}
   </Tag>
 ));
+
+GlassCard.displayName = "GlassCard";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/components/GlassCard.jsx` around lines 6 - 10, The
GlassCard component is created via forwardRef and will appear as "Anonymous" in
React DevTools; set a displayName to improve debugging by adding
GlassCard.displayName = "GlassCard" after the forwardRef declaration (reference
the GlassCard identifier and the forwardRef usage) so the component shows a
readable name in tooling.
example-apps/react-js/src/pages/Editor/EditorControls.jsx (2)

40-59: Consider using onafterprint for more reliable iframe cleanup.

The current 100ms timeout before printing works but is fragile. The iframe is removed immediately after print() is called, which may interrupt the print dialog on some browsers. Using onafterprint would ensure cleanup happens after the user dismisses the print dialog.

♻️ Optional improvement for more reliable cleanup
     // Give browser a moment to render
     setTimeout(() => {
       iframe.contentWindow.focus();
+      iframe.contentWindow.onafterprint = () => {
+        document.body.removeChild(iframe);
+      };
       iframe.contentWindow.print();
-      document.body.removeChild(iframe);
+      // Fallback cleanup for browsers that don't support onafterprint
+      setTimeout(() => {
+        if (iframe.parentNode) {
+          document.body.removeChild(iframe);
+        }
+      }, 60000);
     }, 100);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx` around lines 40 -
59, The printSvg function removes the temporary iframe immediately after calling
print(), which can interrupt the print dialog; update printSvg to attach an
onafterprint handler on iframe.contentWindow (or iframe) that removes the iframe
and cleans up the handler when printing completes, and keep the existing
setTimeout fallback for older browsers; reference the iframe and printSvg
symbols and ensure you remove listeners and the iframe in the onafterprint
callback to avoid leaks.

102-107: Add error handling for clipboard API.

navigator.clipboard.writeText() returns a Promise that can reject if clipboard permissions are denied. Silently failing could confuse users.

♻️ Proposed fix with user feedback
             <Tooltip content="Copy SVG code to clipboard">
-              <button onClick={() => navigator.clipboard.writeText(svg)} className="button">
+              <button
+                onClick={() =>
+                  navigator.clipboard.writeText(svg)
+                    .then(() => alert("SVG copied to clipboard!"))
+                    .catch(() => alert("Failed to copy to clipboard"))
+                }
+                className="button"
+              >
                 <Copy />
                 Copy SVG
               </button>
             </Tooltip>

Consider replacing alert() with a toast notification for better UX.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx` around lines 102 -
107, navigator.clipboard.writeText(svg) can reject (permissions/blocked); update
the onClick handler on the button inside Tooltip to handle Promise rejections by
using async/await or .then/.catch around navigator.clipboard.writeText(svg), and
surface success/failure to the user (replace any alert usage with the app's
toast/notification API) so Copy button (and Copy component) shows a success
toast on resolve and an error toast on rejection with a helpful message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example-apps/react-js/src/components/GlassModal.jsx`:
- Around line 34-39: The modal root div rendered in GlassModal.jsx (the element
using className={styles.backdrop}, role="dialog", and
onClick={handleBackdropClick}) is missing the aria-modal attribute; update that
element to include aria-modal="true" so assistive technologies know background
content is inert (keep the existing role, className, and onClick as-is).
- Around line 60-68: GlassModal.propTypes is missing the style prop declaration;
update GlassModal.propTypes to include style (e.g., style: PropTypes.object or
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array])) so the
component's accepted inline styles are validated, keeping the rest of the
propTypes (isOpen, onClose, children, size, showCloseButton,
closeOnBackdropClick, className) unchanged.

In `@example-apps/react-js/src/components/GlassModal.module.css`:
- Around line 35-43: Rename the keyframe identifiers from fadeIn and scaleIn to
kebab-case (fade-in and scale-in) in GlassModal.module.css and update any local
references that use those names (e.g., animation or animation-name declarations
that reference fadeIn/scaleIn) so they point to fade-in and scale-in; ensure
both the `@keyframes` blocks and all places where those names are referenced
within this CSS module are changed consistently.

In `@example-apps/react-js/src/components/HamburgerMenu.jsx`:
- Around line 10-11: The default icon props are inverted: swap the default JSX
values so OpenMenuIcon defaults to the hamburger (Menu) and CloseMenuIcon
defaults to the close (X); specifically change the defaults for the OpenMenuIcon
and CloseMenuIcon variables (or props) so OpenMenuIcon = <Menu size={20} /> and
CloseMenuIcon = <X size={20} />, and verify wherever the component uses these
props (e.g., the toggle render that chooses between OpenMenuIcon and
CloseMenuIcon) still shows the hamburger when closed and the X when open.
- Around line 76-80: The current React.Children.map + React.cloneElement in
HamburgerMenu.jsx overwrites any child's existing className; change the clone
logic to merge the child's existing className with "button" instead of replacing
it by reading child.props.className and concatenating (or using a classnames
utility) when creating the new props in React.cloneElement so existing classes
like styles.themeToggle are preserved.
- Around line 29-39: The outside-click handler closes the menu when the toggle
button (which sits outside the current menuRef) is clicked, causing a mousedown
close then a click reopen; fix by ensuring the outside-click check includes the
toggle: either move menuRef to wrap both the toggle and the <ul> (so menuRef
covers the toggle and menu) or add a separate toggleRef and update the
onClickOutside handler in the useEffect to ignore events when e.target is inside
toggleRef.current (in addition to the existing menuRef check), keeping the same
listener attach/remove logic around onClickOutside and the same dependencies
([isOpen]).

In `@example-apps/react-js/src/global-styles/components.css`:
- Around line 9-11: The CSS rule "button { all: unset; }" wipes inherited font
styles (negating the "font: inherit" set in base.css), so update the styles for
the button selector to preserve font inheritance: either apply "font: inherit"
immediately after the "all: unset" reset for the button selector, or replace
"all: unset" with a targeted reset that excludes font-related properties (e.g.,
reset padding/margin/background/border but keep font), ensuring the unique
selector "button" and the existing "font: inherit" rule are preserved.
- Around line 35-37: The selector `.button *:active` targets descendant elements
rather than the button itself; change the selector to `.button:active` so the
active transform applies to the `.button` element (update the rule that
currently reads `.button *:active` to `.button:active` and keep the transform
declaration intact).
- Around line 30-41: The hover/active rules currently target only the .button
selector causing native <button> elements to miss interactive feedback; update
the selectors so they include bare button elements (e.g., change ".button:hover"
to "button:hover, .button:hover", ".button *:active" to "button *:active,
.button *:active", and ".button *:hover .externalIcon" to "button *:hover
.externalIcon, .button *:hover .externalIcon") so native buttons (such as those
in EditorControls.jsx) get the same hover/active/transform/opacity behavior.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx`:
- Around line 143-148: The Tooltip text is misleading: the Tooltip wrapping the
Save button currently reads "Share the image with others" but the click handler
(setModalOpen) opens the save/download modal; update the Tooltip's content prop
to accurately describe the action (e.g., "Save image" or "Download image") so
Tooltip, the Save icon/component and the span that calls setModalOpen are
consistent; locate the Tooltip around the Save component in EditorControls.jsx
and change its content string accordingly.
- Around line 89-92: The current <button> using href={svgUrl} and
download={`${fileName}.svg`} won't work because those attributes only apply to
anchors; update the JSX in EditorControls.jsx to either replace the <button>
with an <a> element (keep className="button", set href={svgUrl} and
download={`${fileName}.svg`} and preserve the <Download /> and <span>Original
SVG</span> children) or implement an onClick handler (e.g., downloadSvg) that
programmatically creates an <a> with href=svgUrl and download=fileName + '.svg',
clicks it, and cleans up; reference svgUrl and fileName when wiring the handler.
- Around line 20-24: The current useMemo block that creates svgUrl via
URL.createObjectURL(blob) (symbol: svgUrl) leaks memory because the Blob URL is
never revoked; replace this useMemo with a useEffect that creates the Blob and
object URL when svg changes and returns a cleanup function that calls
URL.revokeObjectURL(svgUrl) (and sets svgUrl state/variable to null or triggers
re-create) so the blob is revoked on dependency changes and on unmount; update
any references to svgUrl accordingly and ensure the effect depends on [svg].

In `@example-apps/react-js/src/pages/Editor/EditorControls.module.css`:
- Around line 7-9: The `.hamburger` rule using `top: 10%` is ineffective without
a position and conflicts with the mobile `top: calc(100% + var(--spacing-sm))`
on the parent; either remove the `top: 10%` declaration or make it apply only on
desktop with a proper positioning (e.g., add `position: relative` to
`.hamburger` inside a desktop media query) so it takes effect without overriding
the mobile positioning used by the HamburgerMenu `<ul>`.

In `@example-apps/react-js/src/pages/Editor/index.jsx`:
- Around line 284-292: The keybinding handler in the useEffect that registers
window.addEventListener("keydown", handler) must handle Mac Cmd keys and prevent
the browser default: update the handler to treat (e.ctrlKey || e.metaKey) when
checking for "z" and "y" and call e.preventDefault() before invoking undo() or
redo(); keep undo and redo as the invoked functions and ensure the same updated
handler is removed in the cleanup to avoid leaks.
- Around line 347-354: The Confirm button's onClick should not only call
restoreHistory(initialSnapshot) but also reset the internal history state to
match the visual snapshot: in the Confirm handler (the onClick for the Confirm
button) call the state setters to set history to an array containing
initialSnapshot and set historyIndex to 0 (e.g., setHistory([initialSnapshot])
and setHistoryIndex(0) or the equivalent state updater used in this component)
before closing the modal so undo/redo reflects the reset.
- Around line 272-282: The pointerrawupdate event listener on viewportRef (in
useEffect) doesn't work in Safari; add a fallback by wiring the same handler
into the GlassCard element as an onPointerMove prop so Safari receives pointer
updates: locate the GlassCard JSX where viewportRef is used and add
onPointerMove={onPointerMove} (or equivalent prop name used by that component),
keeping the existing pointerrawupdate listener for high-frequency Chrome updates
and ensuring you don't duplicate handlers or break existing cleanup logic tied
to onPointerMove/viewportRef.

---

Nitpick comments:
In `@example-apps/react-js/src/components/GlassCard.jsx`:
- Around line 6-10: The GlassCard component is created via forwardRef and will
appear as "Anonymous" in React DevTools; set a displayName to improve debugging
by adding GlassCard.displayName = "GlassCard" after the forwardRef declaration
(reference the GlassCard identifier and the forwardRef usage) so the component
shows a readable name in tooling.

In `@example-apps/react-js/src/components/GlassModal.module.css`:
- Around line 23-33: The .closeButton CSS class lacks an explicit background
which can lead to inconsistent visuals; update the .closeButton rule to set an
explicit background (e.g., background: transparent or your design-system close
button color) so it doesn't inherit unexpected styles from global resets, and
ensure the chosen background works with existing padding/alignments in the
.closeButton rule.

In `@example-apps/react-js/src/components/HamburgerMenu.module.css`:
- Around line 41-45: The .open rule is currently defined outside the mobile
media query but has no effect on desktop because .navList is display:flex and
already visible; move the .open selector (the rule setting opacity, visibility,
transform) into the existing mobile-only media query alongside the .navList
mobile rules so .open only applies on small screens and controls the toggled
mobile menu (refer to the .open class and .navList selector to locate the
relevant CSS).
- Around line 99-111: The styles use an ID selector ("#nav-menu a", "a:link",
"a:visited") which couples the CSS module to a hardcoded DOM id; replace the ID
selector with a module-scoped class (e.g., create a .navMenu or .navAnchor class
in HamburgerMenu.module.css and move the anchor rules there) or scope via
:where(.navModuleClass) to keep specificity while staying module-scoped, then
update the component JSX to apply that className to the nav container or anchors
instead of relying on the `#nav-menu` id.

In `@example-apps/react-js/src/global-styles/components.css`:
- Around line 18-21: The CSS has a missing blank line before the standalone
"color: var(--color-text);" declaration (it immediately follows the universal
selector block "* { color: unset; }"), so add a single empty line before the
"color: var(--color-text);" declaration to satisfy stylelint; locate the
declaration by searching for the "color: var(--color-text);" token or the
universal selector "*" and insert the empty line separator so declarations are
properly separated.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx`:
- Around line 40-59: The printSvg function removes the temporary iframe
immediately after calling print(), which can interrupt the print dialog; update
printSvg to attach an onafterprint handler on iframe.contentWindow (or iframe)
that removes the iframe and cleans up the handler when printing completes, and
keep the existing setTimeout fallback for older browsers; reference the iframe
and printSvg symbols and ensure you remove listeners and the iframe in the
onafterprint callback to avoid leaks.
- Around line 102-107: navigator.clipboard.writeText(svg) can reject
(permissions/blocked); update the onClick handler on the button inside Tooltip
to handle Promise rejections by using async/await or .then/.catch around
navigator.clipboard.writeText(svg), and surface success/failure to the user
(replace any alert usage with the app's toast/notification API) so Copy button
(and Copy component) shows a success toast on resolve and an error toast on
rejection with a helpful message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa845163-5a3e-48dc-bef5-44ca0dc780e8

📥 Commits

Reviewing files that changed from the base of the PR and between a5b57b2 and 9120639.

📒 Files selected for processing (13)
  • example-apps/react-js/src/components/GlassCard.jsx
  • example-apps/react-js/src/components/GlassModal.jsx
  • example-apps/react-js/src/components/GlassModal.module.css
  • example-apps/react-js/src/components/HamburgerMenu.jsx
  • example-apps/react-js/src/components/HamburgerMenu.module.css
  • example-apps/react-js/src/components/NavBar.jsx
  • example-apps/react-js/src/components/NavBar.module.css
  • example-apps/react-js/src/global-styles/base.css
  • example-apps/react-js/src/global-styles/components.css
  • example-apps/react-js/src/pages/Editor/Editor.module.css
  • example-apps/react-js/src/pages/Editor/EditorControls.jsx
  • example-apps/react-js/src/pages/Editor/EditorControls.module.css
  • example-apps/react-js/src/pages/Editor/index.jsx

Comment thread example-apps/react-js/src/components/GlassModal.jsx Outdated
Comment thread example-apps/react-js/src/components/GlassModal.jsx Outdated
Comment thread example-apps/react-js/src/components/GlassModal.module.css
Comment thread example-apps/react-js/src/components/HamburgerMenu.jsx Outdated
Comment thread example-apps/react-js/src/components/HamburgerMenu.jsx Outdated
Comment thread example-apps/react-js/src/pages/Editor/EditorControls.jsx Outdated
Comment thread example-apps/react-js/src/pages/Editor/EditorControls.module.css Outdated
Comment thread example-apps/react-js/src/pages/Editor/index.jsx Outdated
Comment thread example-apps/react-js/src/pages/Editor/index.jsx Outdated
Comment thread example-apps/react-js/src/pages/Editor/index.jsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (6)
example-apps/react-js/src/pages/Editor/EditorControls.jsx (2)

150-155: ⚠️ Potential issue | 🟡 Minor

Misleading tooltip text.

The tooltip says "Share the image with others" but clicking opens the download/save modal, not a share dialog. The actual share functionality is on the separate "Share" menu item below.

📝 Proposed fix
-          <Tooltip content="Share the image with others">
+          <Tooltip content="Save or download the image">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx` around lines 150 -
155, The Tooltip text for the save button is misleading; update the Tooltip
content around the Save button (the Tooltip wrapping the span that calls
setModalOpen and contains the <Save /> icon and "Save" label) to accurately
describe the action (e.g., "Save the image" or "Download the image") so it
matches the modal opened by setModalOpen rather than saying "Share the image
with others".

28-32: ⚠️ Potential issue | 🟠 Major

Memory leak: Blob URL is never revoked.

URL.createObjectURL allocates memory that persists until explicitly released. Using useMemo prevents proper cleanup when the component unmounts or when svg changes.

🔧 Proposed fix using useEffect for proper cleanup
-  const svgUrl = useMemo(() => {
-    if (!svg) return null;
-    const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
-    return URL.createObjectURL(blob);
-  }, [svg]);
+  const [svgUrl, setSvgUrl] = useState(null);
+
+  useEffect(() => {
+    if (!svg) {
+      setSvgUrl(null);
+      return;
+    }
+    const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
+    const url = URL.createObjectURL(blob);
+    setSvgUrl(url);
+
+    return () => URL.revokeObjectURL(url);
+  }, [svg]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx` around lines 28 -
32, The current useMemo block creating svgUrl with URL.createObjectURL leaks
memory because blob URLs are never revoked; replace the useMemo with a useEffect
that creates the blob URL when svg changes (using the same blob creation logic)
and sets it into state or a ref (svgUrl), and in the effect cleanup call
URL.revokeObjectURL on the created URL so it is released both when svg changes
and when the component unmounts; reference the existing svgUrl variable, the
useMemo usage, and URL.createObjectURL/URL.revokeObjectURL to locate and update
the code.
example-apps/react-js/src/components/GlassModal.jsx (1)

40-44: ⚠️ Potential issue | 🟡 Minor

Add aria-modal="true" for proper accessibility.

Per issue #114 requirements, the modal should have aria-modal attribute to indicate to assistive technologies that content behind the modal is inert.

♿ Proposed fix
     <div
       className={styles.backdrop}
       onClick={handleBackdropClick}
       role="dialog"
+      aria-modal="true"
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/components/GlassModal.jsx` around lines 40 - 44,
The modal container in GlassModal.jsx (the div using
className={styles.backdrop}, role="dialog", and onClick={handleBackdropClick})
is missing the aria-modal attribute; update that element to include
aria-modal="true" to indicate the rest of the page is inert for assistive
technologies, preserving the existing role, className, and handler.
example-apps/react-js/src/pages/Editor/index.jsx (3)

284-292: ⚠️ Potential issue | 🟡 Minor

Keyboard shortcuts should support Mac and prevent default browser behavior.

Two issues:

  1. Only checks ctrlKey, missing metaKey for Mac users expecting Cmd+Z/Cmd+Y
  2. Missing e.preventDefault() which could cause browser's native undo to also trigger

Additionally, undo and redo are recreated on each render, causing the handler to use stale closures. Consider using useCallback or referencing state directly.

🛠️ Proposed fix
   useEffect(() => {
     const handler = (e) => {
-      if (e.ctrlKey && e.key === "z") undo();
-      if (e.ctrlKey && e.key === "y") redo();
+      const mod = e.ctrlKey || e.metaKey;
+      if (mod && e.key === "z") {
+        e.preventDefault();
+        undo();
+      }
+      if (mod && e.key === "y") {
+        e.preventDefault();
+        redo();
+      }
     };
     window.addEventListener("keydown", handler);
     return () => window.removeEventListener("keydown", handler);
   }, [historyIndex, history]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/index.jsx` around lines 284 - 292,
Update the keydown handler inside the useEffect that attaches to window (the
anonymous handler passed to window.addEventListener) so it checks for e.ctrlKey
|| e.metaKey (to support Cmd on Mac) and calls e.preventDefault() when
intercepting undo/redo; also ensure the handler uses stable undo/redo references
by memoizing those functions with useCallback or by reading them from refs so
the effect’s dependency array doesn’t capture stale closures (adjust the
useEffect dependencies accordingly and keep window.removeEventListener in the
cleanup).

272-282: ⚠️ Potential issue | 🟠 Major

Stale closure: onPointerMove is not in dependency array.

The useEffect registers onPointerMove as an event listener but has an empty dependency array. Since onPointerMove references pointerState, pinchRef, activePointersRef, and updateTransform, the handler will use stale closures if any of these change.

Additionally, this listener won't work in Safari (as noted in past reviews). Add onPointerMove as a prop fallback on the GlassCard element.

🔧 Proposed fix
   useEffect(() => {
     const el = viewportRef.current;
     if (!el) return;

     el.addEventListener("pointerrawupdate", onPointerMove);

     return () => {
       el.removeEventListener("pointerrawupdate", onPointerMove);
     };
-  }, []);
+  }, [onPointerMove]);

And for Safari fallback, add to the GlassCard element (around line 361-368):

         <GlassCard
           className={`flex-center ${styles.viewport}`}
           ref={viewportRef}
           onWheel={handleWheel}
           onPointerDown={onPointerDown}
+          onPointerMove={onPointerMove}
           onPointerUp={onPointerUp}
           onPointerCancel={onPointerUp}
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/index.jsx` around lines 272 - 282, The
effect registers onPointerMove with an empty dependency array causing a stale
closure; wrap or memoize onPointerMove using useCallback with its real
dependencies (pointerState, pinchRef, activePointersRef, updateTransform) so it
stays up-to-date, then include that stable onPointerMove in the useEffect
dependency array that uses viewportRef; additionally pass this onPointerMove as
a prop to the GlassCard component (so GlassCard can attach it as a pointermove
fallback for Safari) and ensure GlassCard uses that prop to register a
pointermove listener.

346-353: ⚠️ Potential issue | 🟠 Major

Reset confirmation doesn't update history state, causing undo/redo inconsistency.

When the user confirms reset, restoreHistory(initialSnapshot) only updates the visual state (CSS classes), but history and historyIndex remain unchanged. After reset, pressing undo will restore the previous colored state unexpectedly.

🛠️ Proposed fix
             <button onClick={() => {
                 restoreHistory(initialSnapshot);
+                setHistory([initialSnapshot]);
+                setHistoryIndex(0);
                 setModalOpen(false);
               }}
               className="button"
             >
               Confirm
             </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/index.jsx` around lines 346 - 353,
When confirming reset, also update the undo state so history and historyIndex
reflect the new snapshot: after calling restoreHistory(initialSnapshot) in the
Confirm button handler, set the history to an array containing initialSnapshot
and set historyIndex to 0 (or whatever index you use for the current entry) so
future undo/redo behaves correctly; keep setModalOpen(false) as-is. Locate the
Confirm button handler where restoreHistory(initialSnapshot) and
setModalOpen(false) are invoked and add the state updates for history and
historyIndex (use the existing state setters or dispatch functions for history
and historyIndex).
🧹 Nitpick comments (3)
example-apps/react-js/src/pages/Editor/EditorControls.jsx (2)

135-145: Consider disabling undo/redo buttons when actions are unavailable.

The buttons are always enabled, but clicking them when there's nothing to undo/redo has no effect, which may confuse users. Consider passing canUndo/canRedo props to control the disabled state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx` around lines 135 -
145, The undo/redo buttons in EditorControls.jsx always remain enabled; update
the button elements that call onUndo and onRedo to accept and use boolean props
(e.g., canUndo and canRedo) and set the button disabled attribute accordingly,
so the Undo/Redo buttons are disabled when actions are unavailable; also ensure
the Tooltip still displays (wrap the button but keep the tooltip content) and
propagate appropriate aria-disabled or title text if needed for accessibility.

21-26: Missing error handling for clipboard API.

The navigator.clipboard.writeText promise has no .catch() handler. If clipboard access fails (e.g., permissions denied), the error is silently swallowed.

🔧 Proposed fix
   const copySvg = () => {
     navigator.clipboard.writeText(svg).then(() => {
       setCopied(true);
       setTimeout(() => setCopied(false), 1500);
-    });
+    }).catch((err) => {
+      console.error("Failed to copy SVG:", err);
+      // Optionally show user feedback
+    });
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx` around lines 21 -
26, The copySvg function calls navigator.clipboard.writeText(svg) without
handling rejections; update copySvg to add a .catch handler (or use try/catch if
converted to async) to handle permission or write errors, e.g., log the error
and set an error state or user-visible feedback instead of silently swallowing
it; keep the existing success flow (setCopied(true) and setTimeout) and ensure
the error path clears any transient UI if needed (referencing copySvg and
setCopied to locate the code).
example-apps/react-js/src/components/GlassModal.jsx (1)

8-69: PropTypes validation is missing entirely.

The component accepts multiple props (isOpen, onClose, children, size, showCloseButton, closeOnBackdropClick, className, style) but has no PropTypes block for validation.

🔧 Proposed fix: Add PropTypes
+GlassModal.propTypes = {
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  children: PropTypes.node.isRequired,
+  size: PropTypes.oneOfType([
+    PropTypes.oneOf(["sm", "md", "lg"]),
+    PropTypes.string,
+  ]),
+  showCloseButton: PropTypes.bool,
+  closeOnBackdropClick: PropTypes.bool,
+  className: PropTypes.string,
+  style: PropTypes.object,
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/components/GlassModal.jsx` around lines 8 - 69, Add
PropTypes validation for the GlassModal component: import PropTypes and define
GlassModal.propTypes validating isOpen (bool), onClose (func), children (node),
size (oneOf or string), showCloseButton (bool), closeOnBackdropClick (bool),
className (string), and style (object), and also add GlassModal.defaultProps for
optional defaults (size, showCloseButton, closeOnBackdropClick, className,
style) to match the existing defaults in the function signature; reference the
exported function GlassModal to attach these propTypes/defaultProps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx`:
- Line 148: The className is using a string literal instead of resolving the CSS
module, so `styles.hamburger` is rendered as text; update the HamburgerMenu prop
(component HamburgerMenu, prop className) to use the actual CSS module
value—either pass styles.hamburger directly or use a template literal with
${styles.hamburger}—so the correct class name from the styles object is applied.

---

Duplicate comments:
In `@example-apps/react-js/src/components/GlassModal.jsx`:
- Around line 40-44: The modal container in GlassModal.jsx (the div using
className={styles.backdrop}, role="dialog", and onClick={handleBackdropClick})
is missing the aria-modal attribute; update that element to include
aria-modal="true" to indicate the rest of the page is inert for assistive
technologies, preserving the existing role, className, and handler.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx`:
- Around line 150-155: The Tooltip text for the save button is misleading;
update the Tooltip content around the Save button (the Tooltip wrapping the span
that calls setModalOpen and contains the <Save /> icon and "Save" label) to
accurately describe the action (e.g., "Save the image" or "Download the image")
so it matches the modal opened by setModalOpen rather than saying "Share the
image with others".
- Around line 28-32: The current useMemo block creating svgUrl with
URL.createObjectURL leaks memory because blob URLs are never revoked; replace
the useMemo with a useEffect that creates the blob URL when svg changes (using
the same blob creation logic) and sets it into state or a ref (svgUrl), and in
the effect cleanup call URL.revokeObjectURL on the created URL so it is released
both when svg changes and when the component unmounts; reference the existing
svgUrl variable, the useMemo usage, and URL.createObjectURL/URL.revokeObjectURL
to locate and update the code.

In `@example-apps/react-js/src/pages/Editor/index.jsx`:
- Around line 284-292: Update the keydown handler inside the useEffect that
attaches to window (the anonymous handler passed to window.addEventListener) so
it checks for e.ctrlKey || e.metaKey (to support Cmd on Mac) and calls
e.preventDefault() when intercepting undo/redo; also ensure the handler uses
stable undo/redo references by memoizing those functions with useCallback or by
reading them from refs so the effect’s dependency array doesn’t capture stale
closures (adjust the useEffect dependencies accordingly and keep
window.removeEventListener in the cleanup).
- Around line 272-282: The effect registers onPointerMove with an empty
dependency array causing a stale closure; wrap or memoize onPointerMove using
useCallback with its real dependencies (pointerState, pinchRef,
activePointersRef, updateTransform) so it stays up-to-date, then include that
stable onPointerMove in the useEffect dependency array that uses viewportRef;
additionally pass this onPointerMove as a prop to the GlassCard component (so
GlassCard can attach it as a pointermove fallback for Safari) and ensure
GlassCard uses that prop to register a pointermove listener.
- Around line 346-353: When confirming reset, also update the undo state so
history and historyIndex reflect the new snapshot: after calling
restoreHistory(initialSnapshot) in the Confirm button handler, set the history
to an array containing initialSnapshot and set historyIndex to 0 (or whatever
index you use for the current entry) so future undo/redo behaves correctly; keep
setModalOpen(false) as-is. Locate the Confirm button handler where
restoreHistory(initialSnapshot) and setModalOpen(false) are invoked and add the
state updates for history and historyIndex (use the existing state setters or
dispatch functions for history and historyIndex).

---

Nitpick comments:
In `@example-apps/react-js/src/components/GlassModal.jsx`:
- Around line 8-69: Add PropTypes validation for the GlassModal component:
import PropTypes and define GlassModal.propTypes validating isOpen (bool),
onClose (func), children (node), size (oneOf or string), showCloseButton (bool),
closeOnBackdropClick (bool), className (string), and style (object), and also
add GlassModal.defaultProps for optional defaults (size, showCloseButton,
closeOnBackdropClick, className, style) to match the existing defaults in the
function signature; reference the exported function GlassModal to attach these
propTypes/defaultProps.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx`:
- Around line 135-145: The undo/redo buttons in EditorControls.jsx always remain
enabled; update the button elements that call onUndo and onRedo to accept and
use boolean props (e.g., canUndo and canRedo) and set the button disabled
attribute accordingly, so the Undo/Redo buttons are disabled when actions are
unavailable; also ensure the Tooltip still displays (wrap the button but keep
the tooltip content) and propagate appropriate aria-disabled or title text if
needed for accessibility.
- Around line 21-26: The copySvg function calls
navigator.clipboard.writeText(svg) without handling rejections; update copySvg
to add a .catch handler (or use try/catch if converted to async) to handle
permission or write errors, e.g., log the error and set an error state or
user-visible feedback instead of silently swallowing it; keep the existing
success flow (setCopied(true) and setTimeout) and ensure the error path clears
any transient UI if needed (referencing copySvg and setCopied to locate the
code).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a26c2051-2d70-4a50-8cfe-f5473edf9eb3

📥 Commits

Reviewing files that changed from the base of the PR and between 9120639 and d6dbf96.

📒 Files selected for processing (4)
  • docker-compose.yml
  • example-apps/react-js/src/components/GlassModal.jsx
  • example-apps/react-js/src/pages/Editor/EditorControls.jsx
  • example-apps/react-js/src/pages/Editor/index.jsx

Comment thread example-apps/react-js/src/pages/Editor/EditorControls.jsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
example-apps/react-js/src/pages/Editor/index.jsx (2)

272-282: ⚠️ Potential issue | 🟠 Major

Add a standard pointermove fallback for browsers without pointerrawupdate.

This is still Safari / older-Firefox incompatible: the raw-update listener is registered unconditionally, and the viewport never receives onPointerMove. In unsupported browsers, drag and pinch stop updating.

🛠️ Proposed fix
+const supportsRawUpdate =
+  typeof window !== "undefined" && "onpointerrawupdate" in window;
+
   // Speed boost - better than React version
   useEffect(() => {
     const el = viewportRef.current;
-    if (!el) return;
+    if (!el || !supportsRawUpdate) return;

     el.addEventListener("pointerrawupdate", onPointerMove);

     return () => {
       el.removeEventListener("pointerrawupdate", onPointerMove);
@@
         <GlassCard
           className={`flex-center ${styles.viewport}`}
           ref={viewportRef}
           onWheel={handleWheel}
           onPointerDown={onPointerDown}
+          onPointerMove={supportsRawUpdate ? undefined : onPointerMove}
           onPointerUp={onPointerUp}
           onPointerCancel={onPointerUp}
         >
Does the `pointerrawupdate` Pointer Events API fire in current Safari and Firefox releases, and is `pointermove` the recommended fallback when it is unsupported?

Also applies to: 370-376

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/index.jsx` around lines 272 - 282, The
effect currently always registers "pointerrawupdate" on viewportRef.current, so
browsers that don't support pointerrawupdate (Safari/older Firefox) never get
pointer updates; update the useEffect that references viewportRef and
onPointerMove to feature-detect and register "pointerrawupdate" only if
supported (e.g., check "onpointerrawupdate" in Element.prototype or
"PointerEvent" in window && "onpointerrawupdate" in
document.createElement("div")), otherwise register the standard "pointermove" as
a fallback; ensure the cleanup removes whichever listener was added
(removeEventListener for the same event string used) and apply the same
conditional registration logic to the other similar effect that also uses
viewportRef and onPointerMove.

355-358: ⚠️ Potential issue | 🟠 Major

Record reset in history instead of only repainting the SVG.

restoreHistory(initialSnapshot) changes the DOM, but history and historyIndex still point at the pre-reset timeline. That leaves undo/redo out of sync with what the user sees. If reset should be undoable, append it as the next history entry here.

🛠️ Proposed fix
             <button onClick={() => {
                 restoreHistory(initialSnapshot);
+                const newHistory = [
+                  ...history.slice(0, historyIndex + 1),
+                  initialSnapshot,
+                ];
+                setHistory(newHistory);
+                setHistoryIndex(newHistory.length - 1);
                 setModalOpen(false);
               }}
               className="button"
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/index.jsx` around lines 355 - 358, The
reset button calls restoreHistory(initialSnapshot) but doesn't update the undo
stack; update the history state so the reset becomes an undoable action by
appending the new snapshot to the history and advancing historyIndex (or
replacing the tail if you maintain redo semantics). Concretely, in the button
handler (same block that calls restoreHistory(initialSnapshot) and
setModalOpen(false)), push initialSnapshot (or a serialized snapshot used by
your history logic) onto the history array used by your Editor state and set
historyIndex to point at that new entry so undo/redo reflect the reset; ensure
you use the same helpers/state mutators that manage history in this component so
behavior stays consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example-apps/react-js/src/pages/Editor/index.jsx`:
- Around line 220-232: Before mutating history or DOM, bail if the tapped region
is already colored: check shape.classList.contains(styles.coloredRegion) and
return early if true. In the handler containing shape, svgRoot,
styles.coloredRegion, currentShapes, history, historyIndex, setHistory and
setHistoryIndex, add this guard before calling shape.classList.add(...) so you
don’t push an identical snapshot; alternatively compute the post-click
currentShapes and compare to history[historyIndex] and skip
setHistory/setHistoryIndex when they are identical.
- Around line 286-297: The keyboard handler currently calls e.preventDefault()
for any modifier key and swallows all Cmd/Ctrl shortcuts; change the logic in
the handler function so you first ignore events coming from editable targets
(check e.target.isContentEditable or tagName input/textarea/select) and then
only call e.preventDefault() when you detect a handled combo: Ctrl/Cmd+Z for
undo, Ctrl/Cmd+Y for redo, and Shift+Cmd+Z for macOS redo; update the key checks
to detect e.shiftKey for Shift+Cmd+Z and ensure other modifier combos (e.g.,
Cmd+S) are not prevented.

---

Duplicate comments:
In `@example-apps/react-js/src/pages/Editor/index.jsx`:
- Around line 272-282: The effect currently always registers "pointerrawupdate"
on viewportRef.current, so browsers that don't support pointerrawupdate
(Safari/older Firefox) never get pointer updates; update the useEffect that
references viewportRef and onPointerMove to feature-detect and register
"pointerrawupdate" only if supported (e.g., check "onpointerrawupdate" in
Element.prototype or "PointerEvent" in window && "onpointerrawupdate" in
document.createElement("div")), otherwise register the standard "pointermove" as
a fallback; ensure the cleanup removes whichever listener was added
(removeEventListener for the same event string used) and apply the same
conditional registration logic to the other similar effect that also uses
viewportRef and onPointerMove.
- Around line 355-358: The reset button calls restoreHistory(initialSnapshot)
but doesn't update the undo stack; update the history state so the reset becomes
an undoable action by appending the new snapshot to the history and advancing
historyIndex (or replacing the tail if you maintain redo semantics). Concretely,
in the button handler (same block that calls restoreHistory(initialSnapshot) and
setModalOpen(false)), push initialSnapshot (or a serialized snapshot used by
your history logic) onto the history array used by your Editor state and set
historyIndex to point at that new entry so undo/redo reflect the reset; ensure
you use the same helpers/state mutators that manage history in this component so
behavior stays consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cea48f83-da30-465f-9531-36fd9812840b

📥 Commits

Reviewing files that changed from the base of the PR and between d6dbf96 and 0006934.

📒 Files selected for processing (1)
  • example-apps/react-js/src/pages/Editor/index.jsx

Comment thread example-apps/react-js/src/pages/Editor/index.jsx Outdated
Comment thread example-apps/react-js/src/pages/Editor/index.jsx
@Ryan-Millard

Copy link
Copy Markdown
Owner Author

@Krasner, please will you give your opinion on this. I'd just like to know if this is a good addition and also if there is anything you think should be added or changed.

@Krasner

Krasner commented Mar 12, 2026

Copy link
Copy Markdown
Collaborator

@Krasner, please will you give your opinion on this. I'd just like to know if this is a good addition and also if there is anything you think should be added or changed.

Looks good. Just tried it out.

@Ryan-Millard Ryan-Millard changed the title feat(example-app): (React) Editor page overhaul - saving and history management feat(example-app): (React) Editor page upgrades - saving and history management Mar 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
example-apps/react-js/src/pages/Editor/EditorControls.jsx (1)

31-35: ⚠️ Potential issue | 🟠 Major

svgUrl object URL is still leaked (no cleanup on change/unmount).

Line 34 creates a persistent blob URL, but there is no matching cleanup for this specific svgUrl. This was previously reported and is still present.

🔧 Proposed fix
-import { useMemo, useState, useId } from "react";
+import { useEffect, useState, useId } from "react";
@@
-  const svgUrl = useMemo(() => {
-    if (!svg) return null;
-    const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
-    return URL.createObjectURL(blob);
-  }, [svg]);
+  const [svgUrl, setSvgUrl] = useState(null);
+
+  useEffect(() => {
+    if (!svg) {
+      setSvgUrl(null);
+      return;
+    }
+    const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
+    const url = URL.createObjectURL(blob);
+    setSvgUrl(url);
+    return () => URL.revokeObjectURL(url);
+  }, [svg]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx` around lines 31 -
35, The blob URL created in the useMemo for svgUrl (via URL.createObjectURL) is
never revoked, leaking object URLs; update the hook to revoke the previous URL
when svg changes and on unmount by storing the created URL (svgUrl) and calling
URL.revokeObjectURL(oldUrl) before creating a new one and inside a cleanup
function (return cleanup) from useMemo or switch to useEffect that creates the
blob URL from svg and revokes it on cleanup; reference svgUrl,
useMemo/useEffect, URL.createObjectURL and URL.revokeObjectURL when locating the
fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example-apps/react-js/src/pages/Editor/EditorControls.module.css`:
- Around line 46-53: Update the .srOnly CSS rule to replace the deprecated clip
property with a modern clip-path usage: remove the clip declaration and add
clip-path: inset(50%); retain the existing positioning, size, padding, margin,
overflow, white-space and border rules so the utility remains accessible;
optionally include -webkit-clip-path for broader browser support.

---

Duplicate comments:
In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx`:
- Around line 31-35: The blob URL created in the useMemo for svgUrl (via
URL.createObjectURL) is never revoked, leaking object URLs; update the hook to
revoke the previous URL when svg changes and on unmount by storing the created
URL (svgUrl) and calling URL.revokeObjectURL(oldUrl) before creating a new one
and inside a cleanup function (return cleanup) from useMemo or switch to
useEffect that creates the blob URL from svg and revokes it on cleanup;
reference svgUrl, useMemo/useEffect, URL.createObjectURL and URL.revokeObjectURL
when locating the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 82f1bf20-debe-4a2d-accb-9723feb2a357

📥 Commits

Reviewing files that changed from the base of the PR and between 0006934 and fd580a6.

📒 Files selected for processing (6)
  • example-apps/react-js/src/components/GlassModal.jsx
  • example-apps/react-js/src/components/GlassSwitch.module.css
  • example-apps/react-js/src/global-styles/base.css
  • example-apps/react-js/src/global-styles/utilities/layout.css
  • example-apps/react-js/src/pages/Editor/EditorControls.jsx
  • example-apps/react-js/src/pages/Editor/EditorControls.module.css
💤 Files with no reviewable changes (1)
  • example-apps/react-js/src/components/GlassSwitch.module.css
✅ Files skipped from review due to trivial changes (1)
  • example-apps/react-js/src/global-styles/utilities/layout.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • example-apps/react-js/src/global-styles/base.css
  • example-apps/react-js/src/components/GlassModal.jsx

Comment thread example-apps/react-js/src/pages/Editor/EditorControls.module.css
@Ryan-Millard
Ryan-Millard force-pushed the feat/example-app/react/download-svg branch from 8524a23 to 6389ea8 Compare March 21, 2026 23:12
@Ryan-Millard
Ryan-Millard force-pushed the feat/example-app/react/download-svg branch from 3b39f5c to 184e7ca Compare March 21, 2026 23:23
@Ryan-Millard Ryan-Millard changed the title feat(example-app): (React) Editor page upgrades - saving and history management feat(example-app): (React) Editor page upgrades - saving, histories, fullscreen, bug fixes Apr 4, 2026
@Ryan-Millard

Copy link
Copy Markdown
Owner Author

@coderabbitai are you able to review this or has the limit been hit?

@coderabbitai

coderabbitai Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

@Ryan-Millard, of course! I'll kick off a review of this PR right away. 🐇


🧠 Learnings used
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2026-04-01T20:45:20.706Z
Learning: In the Ryan-Millard/Img2Num repository, a PR body containing generic boilerplate such as "This contribution was made as part of an open-source documentation improvement initiative. All changes are meant to be helpful additions to the project." is a strong signal of an AI-generated slop PR and should be explicitly flagged during review.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Ryan-Millard Ryan-Millard added CSS and removed c/c++ Changes to C or C++ files ci labels Apr 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
example-apps/react-js/src/components/GlassModal.jsx (1)

1-1: ⚠️ Potential issue | 🟠 Major

The dialog is still unnamed.

role="dialog" without aria-modal and an accessible name misses the #114 requirements. EditorControls.jsx already passes title="Save image", but this component drops it, so assistive tech gets an unnamed dialog.

♿ Proposed fix
-import { useEffect } from "react";
+import { useEffect, useId } from "react";
 ...
 export default function GlassModal({
   isOpen,
   onClose,
   children,
+  title,
   size = "lg", // sm | md | lg | any CSS width
   showCloseButton = true,
   closeOnBackdropClick = true,
   className = "",
   style = {},
 }) {
+  const titleId = useId();
+
 ...
-    <div className={styles.backdrop} onClick={handleBackdropClick} role="dialog">
+    <div
+      className={styles.backdrop}
+      onClick={handleBackdropClick}
+      role="dialog"
+      aria-modal="true"
+      aria-labelledby={title ? titleId : undefined}
+    >
       <GlassCard
         as="div"
         className={`${styles.modal} ${className}`}
         style={{
           flex: "0 0 auto",
           width: width,
           ...style,
         }}
       >
+        {title ? <h2 id={titleId}>{title}</h2> : null}

Also applies to: 8-17, 37-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/components/GlassModal.jsx` at line 1, The modal
rendered by the GlassModal component is missing an accessible name and
aria-modal; update the GlassModal component to accept the title prop (passed
from EditorControls.jsx as title="Save image") and apply it as an accessible
name (either aria-label={title} or aria-labelledby pointing to a visible header
element) and add aria-modal="true" alongside role="dialog" so assistive tech can
announce it; ensure the header element (if used) has a matching id and that the
component signature (GlassModal / props) uses the title prop when rendering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example-apps/react-js/src/hooks/useFullscreen.js`:
- Around line 19-33: The open() function currently pushes a history entry and
adds a popstate listener before fullscreen actually starts, and close() only
removes the listener so history accumulates; update useFullscreen so it
registers a document-level "fullscreenchange" handler that pushes a history
state when the element actually enters fullscreen and removes/pops that history
entry when it actually exits, and remove the premature window.history.pushState
call from open() and the blind listener removal in close(); also ensure the
Escape key handler in useFullscreen is gated by actual fullscreen state (or
removed) so it does not race with the Escape handler in GlassModal.jsx—use
document.fullscreenElement (or vendor-prefixed equivalents) inside the
fullscreenchange handler to decide when to push/pop history and when to run
close().

In `@example-apps/react-js/src/pages/Editor/EditorControls.jsx`:
- Line 4: Remove the unused blob URL plumbing by deleting the state/variable
svgUrl and the useEffect that creates/revokes the object URL (the effect that
calls URL.createObjectURL and URL.revokeObjectURL) so nothing references svgUrl;
update any related declarations (e.g., the useState/useId lines that declare
svgUrl) and ensure no other code in EditorControls.jsx references svgUrl or the
blob URL logic (remove createObjectURL/revokeObjectURL calls and their
imports/usages).
- Around line 79-125: The save modal (GlassModal) currently only offers SVG, raw
text, copySvg and printSvg but lacks PNG/JPG/PDF export required to close issue
`#85`; add UI buttons alongside the existing list items and implement handlers
exportRaster(format) and exportPDF() (or exportDocument()) that take the current
svg and fileName, render the SVG onto a canvas, then use
canvas.toBlob()/toDataURL to produce PNG/JPEG and call the existing download
helper, and for PDF use a lightweight approach (e.g., draw the canvas image into
a jsPDF or use PDFKit/browser API) to produce a PDF blob and download it; wire
the new buttons to call exportRaster("image/png"), exportRaster("image/jpeg")
and exportPDF(), and ensure accessibility props (aria-pressed/aria-hidden) match
the pattern used by copySvg/printSvg.

In `@example-apps/react-js/src/pages/Editor/index.jsx`:
- Around line 122-131: Pinch math assumes transform-origin at (0,0); because
.inner now uses transform-origin:center center the midpoint calculations drift.
Update the coordinate conversions in the pinch handlers (where
transformRef.current and pinchRef.current.startCx/startCy are set and where
nextTx/nextTy are computed) to account for the element’s transform-origin
(element center) by converting the midpoint into the element’s local coordinates
relative to that origin (subtract elementCenter before dividing by scale, and
add back when computing tx/ty), or revert .inner to transform-origin: 0 0 so the
existing formulas remain valid.
- Line 17: Remove the stray useFullscreen() call that mounts Escape/fullscreen
effects but whose destructured values (ref, toggle) are never used; either
delete the line "const { ref, toggle } = useFullscreen();" from the Editor
component or replace it with the intended usage (attach ref to the editor
element and call toggle where needed) so the hook isn't mounted unnecessarily
and duplicate listeners/lint errors are eliminated.

---

Duplicate comments:
In `@example-apps/react-js/src/components/GlassModal.jsx`:
- Line 1: The modal rendered by the GlassModal component is missing an
accessible name and aria-modal; update the GlassModal component to accept the
title prop (passed from EditorControls.jsx as title="Save image") and apply it
as an accessible name (either aria-label={title} or aria-labelledby pointing to
a visible header element) and add aria-modal="true" alongside role="dialog" so
assistive tech can announce it; ensure the header element (if used) has a
matching id and that the component signature (GlassModal / props) uses the title
prop when rendering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a57606c-d0f0-48c2-b77f-ddfee4fb2a2f

📥 Commits

Reviewing files that changed from the base of the PR and between fd580a6 and 623e83f.

📒 Files selected for processing (9)
  • example-apps/react-js/src/components/GlassCard.jsx
  • example-apps/react-js/src/components/GlassModal.jsx
  • example-apps/react-js/src/components/GlassModal.module.css
  • example-apps/react-js/src/global-styles/base.css
  • example-apps/react-js/src/hooks/useFullscreen.js
  • example-apps/react-js/src/pages/Editor/Editor.module.css
  • example-apps/react-js/src/pages/Editor/EditorControls.jsx
  • example-apps/react-js/src/pages/Editor/EditorControls.module.css
  • example-apps/react-js/src/pages/Editor/index.jsx
✅ Files skipped from review due to trivial changes (1)
  • example-apps/react-js/src/pages/Editor/EditorControls.module.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • example-apps/react-js/src/global-styles/base.css
  • example-apps/react-js/src/components/GlassCard.jsx

Comment thread example-apps/react-js/src/hooks/useFullscreen.js Outdated
Comment thread example-apps/react-js/src/pages/Editor/EditorControls.jsx Outdated
Comment thread example-apps/react-js/src/pages/Editor/EditorControls.jsx Outdated
Comment thread example-apps/react-js/src/pages/Editor/index.jsx Outdated
Comment on lines +122 to +131
const t = transformRef.current;

pinchRef.current.active = true;
pinchRef.current.startDist = Math.hypot(p1.x - p2.x, p1.y - p2.y);
pinchRef.current.startScale = transform.scale;
pinchRef.current.startTx = transform.tx;
pinchRef.current.startTy = transform.ty;
pinchRef.current.startScale = t.scale;
pinchRef.current.startTx = t.tx;
pinchRef.current.startTy = t.ty;

// content coordinate under the pinch midpoint at start
pinchRef.current.startCx = (midX - transform.tx) / transform.scale;
pinchRef.current.startCy = (midY - transform.ty) / transform.scale;
pinchRef.current.startCx = (midX - t.tx) / t.scale;
pinchRef.current.startCy = (midY - t.ty) / t.scale;

@coderabbitai coderabbitai Bot Apr 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Pinch zoom still assumes a top-left transform origin.

startCx/startCy = (mid - tx) / scale and nextTx/nextTy = mid - startC * scale are only correct when .inner scales from (0,0). After Editor.module.css switched .inner to transform-origin: center center, the pinch midpoint will drift instead of staying under the fingers. Either revert the CSS origin or include the element center in these coordinate conversions.

Also applies to: 164-173

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example-apps/react-js/src/pages/Editor/index.jsx` around lines 122 - 131,
Pinch math assumes transform-origin at (0,0); because .inner now uses
transform-origin:center center the midpoint calculations drift. Update the
coordinate conversions in the pinch handlers (where transformRef.current and
pinchRef.current.startCx/startCy are set and where nextTx/nextTy are computed)
to account for the element’s transform-origin (element center) by converting the
midpoint into the element’s local coordinates relative to that origin (subtract
elementCenter before dividing by scale, and add back when computing tx/ty), or
revert .inner to transform-origin: 0 0 so the existing formulas remain valid.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Show me how to include the element center.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Ryan-Millard, here's how to include the element center (ox, oy) in the pinch coordinate conversions.


Why it drifts

With transform-origin: center center, the browser's effective transform is:

screen_x = ox + tx + (local_x - ox) × scale
screen_y = oy + ty + (local_y - oy) × scale

where ox = element.offsetWidth / 2, oy = element.offsetHeight / 2. The original formula assumed ox = oy = 0 (i.e., top-left origin).


Fix

Step 1 — capture origin at pinch start in onPointerDown:

if (activePointersRef.current.size === 2) {
  const [p1, p2] = Array.from(activePointersRef.current.values());

  const midX = (p1.x + p2.x) / 2;
  const midY = (p1.y + p2.y) / 2;

  const t = transformRef.current;

  // Capture the transform-origin (center of .inner in layout space)
  const innerEl = innerRef.current;
  const ox = innerEl ? innerEl.offsetWidth / 2 : 0;
  const oy = innerEl ? innerEl.offsetHeight / 2 : 0;

  pinchRef.current.active     = true;
  pinchRef.current.startDist  = Math.hypot(p1.x - p2.x, p1.y - p2.y);
  pinchRef.current.startScale = t.scale;
  pinchRef.current.startTx    = t.tx;
  pinchRef.current.startTy    = t.ty;
  pinchRef.current.ox         = ox;  // ← store origin
  pinchRef.current.oy         = oy;  // ← store origin

  // Convert midpoint to local element space, accounting for center origin:
  //   local = (screen - ox - tx) / scale + ox
  pinchRef.current.startCx = (midX - ox - t.tx) / t.scale + ox;
  pinchRef.current.startCy = (midY - oy - t.ty) / t.scale + oy;

  pointerState.current.moved = true;
}

Step 2 — use the stored origin when computing nextTx/nextTy in onPointerMove:

if (pinchRef.current.startDist > 0) {
  const scaleFactor = dist / pinchRef.current.startDist;
  const nextScale = clamp(pinchRef.current.startScale * scaleFactor, 0.25, 6);

  const { startCx, startCy, ox, oy } = pinchRef.current;

  // Solve for tx: midX = ox + nextTx + (startCx - ox) * nextScale
  const nextTx = midX - ox - (startCx - ox) * nextScale;
  const nextTy = midY - oy - (startCy - oy) * nextScale;

  updateTransform((t) => {
    t.scale = nextScale;
    t.tx    = nextTx;
    t.ty    = nextTy;
  });
}

Quick sanity check

If you set ox = oy = 0, the formulas collapse back to the originals:

  • startCx = (midX - tx) / scale
  • nextTx = midX - startCx * nextScale

So this is a backward-compatible generalisation — if you ever revert the CSS origin back to 0 0 these formulas still work correctly.


🧠 Learnings used
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2026-04-01T20:45:20.706Z
Learning: In the Ryan-Millard/Img2Num repository, a PR body containing generic boilerplate such as "This contribution was made as part of an open-source documentation improvement initiative. All changes are meant to be helpful additions to the project." is a strong signal of an AI-generated slop PR and should be explicitly flagged during review.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Your implementation doesn't work and I don't want to handle it now because it might take a while. Please create an issue for this and then let me know once you have done that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

gh issue create \
  --repo Ryan-Millard/Img2Num \
  --title "bug(example-app/react): Pinch zoom drifts when .inner uses transform-origin: center center" \
  --body "## Summary

Pinch-zoom in the Editor page drifts instead of staying anchored under the fingers when \`.inner\` uses \`transform-origin: center center\` (introduced in this PR).

## Root cause

The coordinate conversions in \`onPointerDown\` and \`onPointerMove\` (in \`example-apps/react-js/src/pages/Editor/index.jsx\`) were written assuming \`transform-origin: 0 0\` (top-left). After \`Editor.module.css\` switched \`.inner\` to \`transform-origin: center center\`, those formulas are no longer correct and the pinch midpoint drifts.

The affected calculations are:
- \`startCx / startCy\` in \`onPointerDown\`
- \`nextTx / nextTy\` in \`onPointerMove\`

## Expected behaviour

The content under the pinch midpoint should remain stationary while the user zooms in or out with two fingers.

## Steps to reproduce

1. Open the Editor page with an uploaded image.
2. Use two fingers to pinch-zoom on a touch device or touch-capable screen.
3. Observe that the content shifts/drifts rather than scaling around the pinch centre.

## Possible fix

The coordinate conversions need to account for the element's transform-origin (its centre point). See the discussion in PR `#278` (comment: https://github.com/Ryan-Millard/Img2Num/pull/278#discussion_r3034928747) for a proposed approach — it needs further work as the initial attempt did not function correctly.

## References

- PR: https://github.com/Ryan-Millard/Img2Num/pull/278
- Review comment: https://github.com/Ryan-Millard/Img2Num/pull/278#discussion_r3034928747

Reported by `@Ryan-Millard`." \
  --assignee Ryan-Millard

Length of output: 50


@Ryan-Millard, done! I've opened issue #294 to track this:

bug(example-app/react): Pinch zoom drifts when .inner uses transform-origin: center center

It's assigned to you and includes a summary of the root cause, steps to reproduce, and a reference back to this PR comment thread for context when you're ready to revisit it.


🧠 Learnings used
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2026-04-01T20:45:20.706Z
Learning: In the Ryan-Millard/Img2Num repository, a PR body containing generic boilerplate such as "This contribution was made as part of an open-source documentation improvement initiative. All changes are meant to be helpful additions to the project." is a strong signal of an AI-generated slop PR and should be explicitly flagged during review.

@Ryan-Millard
Ryan-Millard force-pushed the feat/example-app/react/download-svg branch from 1423f85 to f56519f Compare April 4, 2026 01:54
- useCallback where necessary, remove unused vars, etc.
- Position ToolTips at top
@github-actions github-actions Bot added the c/c++ Changes to C or C++ files label Apr 4, 2026
@Krasner

Krasner commented Apr 4, 2026

Copy link
Copy Markdown
Collaborator

this seems to work well for zooming and panning

@Ryan-Millard Ryan-Millard changed the title feat(example-app): (React) Editor page upgrades - saving, histories, fullscreen, bug fixes feat(example-app): (React) GlassModal and Editor page upgrades (saving, histories, fullscreen, bug fixes) Apr 4, 2026
@Ryan-Millard Ryan-Millard changed the title feat(example-app): (React) GlassModal and Editor page upgrades (saving, histories, fullscreen, bug fixes) feat(example-app): (React) new GlassModal and Editor page upgrades (saving, histories, fullscreen, bug fixes) Apr 4, 2026
- Introduce jsPDF dependency for PDF export
- Add exportRaster function for PNG/JPG downloads
- Reorganize GlassModal into Export, Clipboard, Other sections
- Update CSS for modal layout and responsive toolbar spacing
@Ryan-Millard
Ryan-Millard force-pushed the feat/example-app/react/download-svg branch from 3d3e363 to 4931be6 Compare April 4, 2026 19:12
@Ryan-Millard Ryan-Millard removed the js/ts label Apr 4, 2026
@Ryan-Millard
Ryan-Millard merged commit d46a4c9 into main Apr 4, 2026
14 checks passed
@Ryan-Millard
Ryan-Millard deleted the feat/example-app/react/download-svg branch April 4, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c/c++ Changes to C or C++ files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Panning (shifting) output SVG not working Implement GlassModal feat(example-app): add export options for processed images (print, SVG, PDF)

2 participants