-
Notifications
You must be signed in to change notification settings - Fork 8
Add initial dynamic shapes plugin #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Conversation
WalkthroughThis pull request introduces the Dynamic Shapes WordPress plugin, a new block extension that adds dynamic shape capabilities to core WordPress blocks. The addition includes the complete plugin structure: PHP bootstrap and autoload logic, a self-update mechanism for GitHub-hosted distribution, editor and frontend JavaScript/CSS assets for dynamic shape controls, build configuration (package.json, PostCSS), and plugin documentation. The implementation enables per-corner offset controls, SVG clip-path rendering, border and shadow integration, and preset spacing management for supported blocks. Configuration files establish coding standards and dependency management. Pre-merge checks and finishing touches✅ Passed checks (2 passed)
✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
dynamic-shapes/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (18)
dynamic-shapes/.editorconfig(1 hunks)dynamic-shapes/.gitignore(1 hunks)dynamic-shapes/classes/class-wpcomsp-blocks-self-update.php(1 hunks)dynamic-shapes/dynamic-shapes.php(1 hunks)dynamic-shapes/includes/dynamic-shapes.php(1 hunks)dynamic-shapes/includes/functions.php(1 hunks)dynamic-shapes/package.json(1 hunks)dynamic-shapes/postcss.config.js(1 hunks)dynamic-shapes/readme.txt(1 hunks)dynamic-shapes/src/css/dynamic-shape-blocks.css(1 hunks)dynamic-shapes/src/css/dynamic-shape-controls.css(1 hunks)dynamic-shapes/src/js/extend-blocks.js(1 hunks)dynamic-shapes/src/js/imports/axis-controls.js(1 hunks)dynamic-shapes/src/js/imports/corner-control.js(1 hunks)dynamic-shapes/src/js/imports/corner-icon.js(1 hunks)dynamic-shapes/src/js/imports/get-path.js(1 hunks)dynamic-shapes/src/js/imports/utils.js(1 hunks)dynamic-shapes/src/js/view.js(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-08-05T11:13:58.666Z
Learnt from: nickpagz
Repo: a8cteam51/special-projects-blocks-monorepo PR: 77
File: marquee/src/marquee/block.json:48-52
Timestamp: 2025-08-05T11:13:58.666Z
Learning: wp-scripts build follows specific naming conventions: when editor.scss is imported in edit.js, it compiles to index.css (editor styles), and when style.scss is imported in index.js, it compiles to style-index.css (frontend/editor shared styles). The block.json should reference the final compiled CSS filenames, not the source SCSS filenames.
Applied to files:
dynamic-shapes/src/css/dynamic-shape-blocks.cssdynamic-shapes/package.json
📚 Learning: 2025-08-12T13:37:33.345Z
Learnt from: jkguidaven
Repo: a8cteam51/special-projects-blocks-monorepo PR: 82
File: tabs/src/tabs/block.json:12-14
Timestamp: 2025-08-12T13:37:33.345Z
Learning: The tabs block (wpcomsp/tabs) in the special-projects-blocks-monorepo is a new block, so attribute schema changes don't require deprecation/migration paths.
Applied to files:
dynamic-shapes/src/js/extend-blocks.js
📚 Learning: 2025-08-01T18:54:28.304Z
Learnt from: tommusrhodus
Repo: a8cteam51/special-projects-blocks-monorepo PR: 48
File: scroll-progress-bar/scroll-progress-bar.php:27-30
Timestamp: 2025-08-01T18:54:28.304Z
Learning: For WordPress blocks in this monorepo, the build directory and block.json files are created by GitHub Actions after PR acceptance, not during development, so build/block.json is the correct path even though it doesn't exist in the source code.
Applied to files:
dynamic-shapes/package.json
📚 Learning: 2025-08-05T11:13:58.666Z
Learnt from: nickpagz
Repo: a8cteam51/special-projects-blocks-monorepo PR: 77
File: marquee/src/marquee/block.json:48-52
Timestamp: 2025-08-05T11:13:58.666Z
Learning: In WordPress block development using wp-scripts, the build process automatically handles SCSS to CSS transformation and file renaming. The block.json can reference CSS files (like index.css, style-index.css) even when the source files are SCSS files with different names (like editor.scss, style.scss), as wp-scripts build correctly maps and renames them during compilation.
Applied to files:
dynamic-shapes/package.json
🧬 Code graph analysis (7)
dynamic-shapes/src/js/imports/get-path.js (2)
dynamic-shapes/src/js/view.js (3)
isGroupBlock(25-25)height(145-145)width(144-144)dynamic-shapes/src/js/imports/utils.js (6)
isPresetValue(201-205)isPresetValue(201-205)getComputedPixelValue(45-107)getComputedPixelValue(45-107)getPixelValue(134-192)getPixelValue(134-192)
dynamic-shapes/src/js/imports/corner-control.js (2)
dynamic-shapes/src/js/imports/utils.js (2)
isPresetValue(201-205)isPresetValue(201-205)dynamic-shapes/src/js/imports/corner-icon.js (2)
CornerIcon(4-30)CornerIcon(4-30)
dynamic-shapes/dynamic-shapes.php (1)
dynamic-shapes/classes/class-wpcomsp-blocks-self-update.php (3)
WPCOMSP_Blocks_Self_Update(13-80)get_instance(22-28)hooks(33-35)
dynamic-shapes/src/js/extend-blocks.js (4)
dynamic-shapes/src/js/view.js (8)
path(162-162)values(92-92)width(144-144)height(145-145)style(41-41)style(72-72)borderWidth(168-169)svg(170-170)dynamic-shapes/src/js/imports/get-path.js (3)
dimensions(22-22)getPath(15-110)getPath(15-110)dynamic-shapes/src/js/imports/utils.js (18)
isPresetValue(201-205)isPresetValue(201-205)getComputedPixelValue(45-107)getComputedPixelValue(45-107)getPaddingVar(116-124)getPaddingVar(116-124)resolveColor(233-258)resolveColor(233-258)getPixelValue(134-192)getPixelValue(134-192)presetToCssVar(16-32)presetToCssVar(16-32)useSpacingPresets(265-278)useSpacingPresets(265-278)axisConfig(303-319)axisConfig(303-319)hasAxisValues(288-294)hasAxisValues(288-294)dynamic-shapes/src/js/imports/axis-controls.js (2)
AxisControls(11-53)AxisControls(11-53)
dynamic-shapes/src/js/imports/axis-controls.js (2)
dynamic-shapes/src/js/view.js (2)
corners(93-93)values(92-92)dynamic-shapes/src/js/imports/corner-control.js (2)
CornerControl(17-178)CornerControl(17-178)
dynamic-shapes/src/js/view.js (2)
dynamic-shapes/src/js/imports/utils.js (2)
getPixelValue(134-192)getPixelValue(134-192)dynamic-shapes/src/js/imports/get-path.js (2)
getPath(15-110)getPath(15-110)
dynamic-shapes/includes/dynamic-shapes.php (1)
dynamic-shapes/includes/functions.php (3)
enqueue_script(111-130)get_slug(48-50)enqueue_style(139-154)
| $plugin_filename_parts = explode( '/', $plugin_file ); | ||
|
|
||
| // Ask opsoasis.mystagingwebsite.com if there's an update. | ||
| $response = wp_remote_get( | ||
| 'https://opsoasis.wpspecialprojects.com/wp-json/opsoasis-blocks-version-manager/v1/update-check', | ||
| array( | ||
| 'body' => array( | ||
| 'plugin' => $plugin_filename_parts[0], | ||
| 'version' => $plugin_data['Version'], | ||
| ), | ||
| ) | ||
| ); | ||
|
|
||
| // Bail if this plugin wasn't found on opsoasis.mystagingwebsite.com. | ||
| if ( 404 === wp_remote_retrieve_response_code( $response ) || 202 === wp_remote_retrieve_response_code( $response ) ) { | ||
| return $update; | ||
| } | ||
|
|
||
| $updated_version = wp_remote_retrieve_body( $response ); | ||
| $updated_array = json_decode( $updated_version, true ); | ||
|
|
||
| return array( | ||
| 'slug' => $updated_array['slug'], | ||
| 'version' => $updated_array['version'], | ||
| 'url' => $updated_array['package_url'], | ||
| 'package' => $updated_array['package_url'], | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden the self-update response handling.
If wp_remote_get() returns a WP_Error, a non-JSON body, or a payload missing the expected keys, line 74 tries to read array offsets on null, which triggers a fatal error during the update check. Guard the remote call and decode before dereferencing.
- $response = wp_remote_get(
+ $response = wp_remote_get(
'https://opsoasis.wpspecialprojects.com/wp-json/opsoasis-blocks-version-manager/v1/update-check',
array(
'body' => array(
'plugin' => $plugin_filename_parts[0],
'version' => $plugin_data['Version'],
),
)
);
- if ( 404 === wp_remote_retrieve_response_code( $response ) || 202 === wp_remote_retrieve_response_code( $response ) ) {
+ if ( is_wp_error( $response ) ) {
+ return $update;
+ }
+
+ $response_code = wp_remote_retrieve_response_code( $response );
+
+ if ( 404 === $response_code || 202 === $response_code ) {
return $update;
}
$updated_version = wp_remote_retrieve_body( $response );
- $updated_array = json_decode( $updated_version, true );
+ $updated_array = json_decode( $updated_version, true );
+
+ if ( ! is_array( $updated_array ) || empty( $updated_array['slug'] ) || empty( $updated_array['version'] ) || empty( $updated_array['package_url'] ) ) {
+ return $update;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $plugin_filename_parts = explode( '/', $plugin_file ); | |
| // Ask opsoasis.mystagingwebsite.com if there's an update. | |
| $response = wp_remote_get( | |
| 'https://opsoasis.wpspecialprojects.com/wp-json/opsoasis-blocks-version-manager/v1/update-check', | |
| array( | |
| 'body' => array( | |
| 'plugin' => $plugin_filename_parts[0], | |
| 'version' => $plugin_data['Version'], | |
| ), | |
| ) | |
| ); | |
| // Bail if this plugin wasn't found on opsoasis.mystagingwebsite.com. | |
| if ( 404 === wp_remote_retrieve_response_code( $response ) || 202 === wp_remote_retrieve_response_code( $response ) ) { | |
| return $update; | |
| } | |
| $updated_version = wp_remote_retrieve_body( $response ); | |
| $updated_array = json_decode( $updated_version, true ); | |
| return array( | |
| 'slug' => $updated_array['slug'], | |
| 'version' => $updated_array['version'], | |
| 'url' => $updated_array['package_url'], | |
| 'package' => $updated_array['package_url'], | |
| ); | |
| $plugin_filename_parts = explode( '/', $plugin_file ); | |
| // Ask opsoasis.mystagingwebsite.com if there's an update. | |
| $response = wp_remote_get( | |
| 'https://opsoasis.wpspecialprojects.com/wp-json/opsoasis-blocks-version-manager/v1/update-check', | |
| array( | |
| 'body' => array( | |
| 'plugin' => $plugin_filename_parts[0], | |
| 'version' => $plugin_data['Version'], | |
| ), | |
| ) | |
| ); | |
| if ( is_wp_error( $response ) ) { | |
| return $update; | |
| } | |
| $response_code = wp_remote_retrieve_response_code( $response ); | |
| // Bail if this plugin wasn't found on opsoasis.mystagingwebsite.com. | |
| if ( 404 === $response_code || 202 === $response_code ) { | |
| return $update; | |
| } | |
| $updated_version = wp_remote_retrieve_body( $response ); | |
| $updated_array = json_decode( $updated_version, true ); | |
| if ( ! is_array( $updated_array ) || empty( $updated_array['slug'] ) || empty( $updated_array['version'] ) || empty( $updated_array['package_url'] ) ) { | |
| return $update; | |
| } | |
| return array( | |
| 'slug' => $updated_array['slug'], | |
| 'version' => $updated_array['version'], | |
| 'url' => $updated_array['package_url'], | |
| 'package' => $updated_array['package_url'], | |
| ); |
🤖 Prompt for AI Agents
In dynamic-shapes/classes/class-wpcomsp-blocks-self-update.php around lines 52
to 78, the code assumes wp_remote_get() succeeded and json_decode() returned an
array; this can cause fatal errors when wp_remote_get() returns a WP_Error, a
non-200/OK response, an invalid JSON body, or when expected keys are missing.
Fix: after wp_remote_get() check is_wp_error($response) and return $update on
error; verify wp_remote_retrieve_response_code($response) === 200 before
proceeding; get the body, json_decode it and confirm the result is an array;
ensure keys 'slug','version','package_url' exist (use isset or array_key_exists)
before building the return array; if any check fails, return the original
$update (optionally log the error).
| $dir_path = constant( 'WPCOMSP_DYNAMIC_SHAPES_DIR' ); | ||
| if ( null === $dir_path ) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard constants before calling constant()
Calling constant( 'WPCOMSP_DYNAMIC_SHAPES_DIR' ) (and the URL variant) before verifying that the constants exist will raise a ValueError on PHP 8+, so the current null checks never run. Any call into these helpers before the constants are defined will fatally error instead of safely returning early. Please switch to defined() guards and only read the constant value once it’s known to exist.
- $dir_path = constant( 'WPCOMSP_DYNAMIC_SHAPES_DIR' );
- if ( null === $dir_path ) {
- return null;
- }
+ if ( ! defined( 'WPCOMSP_DYNAMIC_SHAPES_DIR' ) ) {
+ return null;
+ }
+ $dir_path = WPCOMSP_DYNAMIC_SHAPES_DIR;
@@
- $dir_path = constant( 'WPCOMSP_DYNAMIC_SHAPES_DIR' );
- $dir_url = constant( 'WPCOMSP_DYNAMIC_SHAPES_URL' );
- if ( null === $dir_path || null === $dir_url ) {
+ if ( ! defined( 'WPCOMSP_DYNAMIC_SHAPES_DIR' ) || ! defined( 'WPCOMSP_DYNAMIC_SHAPES_URL' ) ) {
return;
}
+ $dir_path = WPCOMSP_DYNAMIC_SHAPES_DIR;
+ $dir_url = WPCOMSP_DYNAMIC_SHAPES_URL;
@@
- $dir_path = constant( 'WPCOMSP_DYNAMIC_SHAPES_DIR' );
- $dir_url = constant( 'WPCOMSP_DYNAMIC_SHAPES_URL' );
- if ( null === $dir_path || null === $dir_url ) {
+ if ( ! defined( 'WPCOMSP_DYNAMIC_SHAPES_DIR' ) || ! defined( 'WPCOMSP_DYNAMIC_SHAPES_URL' ) ) {
return;
}
+ $dir_path = WPCOMSP_DYNAMIC_SHAPES_DIR;
+ $dir_url = WPCOMSP_DYNAMIC_SHAPES_URL;Also applies to: 112-116, 140-143
| label={ cornerLabel } | ||
| value={ values?.[ key ] || '' } | ||
| onChange={ ( value ) => | ||
| onChange( { ...values, [ key ]: value || '' } ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against undefined values before spreading.
values is treated as optional elsewhere (values?.[...]), but spreading an undefined value throws “Cannot convert undefined or null to object.” The first slider interaction when no dynamic shape data exists will crash. Default to an empty object (e.g., { ...( values || {} ), … }) or otherwise ensure values is always defined before spreading.
🤖 Prompt for AI Agents
In dynamic-shapes/src/js/imports/axis-controls.js around lines 41 to 45, the
onChange handler spreads values which can be undefined and cause “Cannot convert
undefined or null to object.” Update the spread to use a safe default (e.g.,
spread ...(values || {}) or ensure values is initialized to an empty object
before use) so the new object is always created from a defined object, then
merge the updated key and value as before.
| <RangeControl | ||
| __next40pxDefaultSize | ||
| __nextHasNoMarginBottom | ||
| className="dynamic-shapes-corner-control__range-control" | ||
| value={ presetIndex !== undefined ? presetIndex + 1 : 0 } | ||
| onChange={ handlePresetChange } | ||
| withInputField={ false } | ||
| aria-valuenow={ | ||
| presetIndex !== undefined ? presetIndex + 1 : 0 | ||
| } | ||
| aria-valuetext={ | ||
| marks[ presetIndex !== undefined ? presetIndex + 1 : 0 ] | ||
| .tooltip | ||
| } | ||
| renderTooltipContent={ ( index ) => | ||
| marks[ ! index ? 0 : index ].tooltip | ||
| } | ||
| min={ 0 } | ||
| max={ marks.length - 1 } | ||
| marks={ marks } | ||
| label={ label } | ||
| hideLabelFromVision | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle empty preset lists safely.
When presets is empty, marks is [], yet the preset slider still renders and immediately evaluates marks[0].tooltip, which throws and breaks the inspector. Either fall back to the custom control when there are no presets or guard all slider logic behind marks.length > 0 so these lookups never execute on an empty array.
🤖 Prompt for AI Agents
In dynamic-shapes/src/js/imports/corner-control.js around lines 140 to 162, the
RangeControl accesses marks[0].tooltip and other marks indices even when presets
(and thus marks) are empty which throws; fix by checking marks.length > 0 before
rendering or accessing marks: render the custom control (fallback) when
marks.length === 0, otherwise render the RangeControl, and for the RangeControl
guard all computed lookups (value, aria-valuenow, aria-valuetext,
renderTooltipContent) to only index marks when marks.length > 0 (use safe
defaults like 0 or an empty string for aria text) so no marks[...] access
happens on an empty array.
| // Recalculate border radius with new computed values. | ||
| const style = getComputedStyle( cachedData.styleElement ); | ||
| cachedData.borderRadius = getBorderRadius( style ); | ||
|
|
||
| // Recalculate border data if present. | ||
| if ( block.dataset.borderWidth ) { | ||
| cachedData.borderData = { | ||
| width: getPixelValue( block.dataset.borderWidth, block ) * 2, | ||
| color: block.dataset.borderColor, | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reset cached border data when the dataset no longer defines a border.
If a block had a border and the user later removes it, block.dataset.borderWidth disappears but cachedData.borderData keeps the old object. Subsequent updates continue to render the stale stroke, so the border can never be cleared on the front end. Set borderData back to null in the else branch so the clip-path renderer can drop the stroke.
- if ( block.dataset.borderWidth ) {
- cachedData.borderData = {
- width: getPixelValue( block.dataset.borderWidth, block ) * 2,
- color: block.dataset.borderColor,
- };
- }
+ if ( block.dataset.borderWidth ) {
+ cachedData.borderData = {
+ width: getPixelValue( block.dataset.borderWidth, block ) * 2,
+ color: block.dataset.borderColor,
+ };
+ } else {
+ cachedData.borderData = null;
+ }🤖 Prompt for AI Agents
In dynamic-shapes/src/js/view.js around lines 71 to 82, if a block previously
had a border and its dataset no longer contains borderWidth,
cachedData.borderData is left unchanged causing stale strokes to persist; update
the code to explicitly set cachedData.borderData = null in the else branch when
block.dataset.borderWidth is absent so the clip-path renderer can drop the
stroke.
| // Handle border if present. | ||
| if ( cachedData.borderData ) { | ||
| const { width: borderWidth, color: borderColor } = | ||
| cachedData.borderData; | ||
| const svg = `<svg width="${ width }" height="${ height }" viewBox="0 0 ${ width } ${ height }" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="${ path }" stroke="${ borderColor }" stroke-width="${ borderWidth }"/></svg>`; | ||
| block.style.background = `url('data:image/svg+xml, ${ svg }')`; | ||
| if ( cachedData.background ) { | ||
| block.style.background += `, ${ cachedData.background }`; | ||
| } | ||
| } | ||
| } else if ( cachedData.isImageBlock ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Encode the SVG stroke color and clear the inline background when no border is present.
The inline SVG background is built with the raw borderColor. Hex values include #, which breaks the data: URI (# starts a fragment); as a result borders disappear on the frontend even though the editor preview works (the editor encodes to %23). Additionally, when a border is removed, we skip the borderData branch entirely and never overwrite the old block.style.background, leaving the previous SVG stroke in place. Encode the color (or the whole SVG) and restore/reset the background when borderData is falsy.
- if ( cachedData.borderData ) {
- const { width: borderWidth, color: borderColor } =
- cachedData.borderData;
- const svg = `<svg width="${ width }" height="${ height }" viewBox="0 0 ${ width } ${ height }" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="${ path }" stroke="${ borderColor }" stroke-width="${ borderWidth }"/></svg>`;
- block.style.background = `url('data:image/svg+xml, ${ svg }')`;
- if ( cachedData.background ) {
- block.style.background += `, ${ cachedData.background }`;
- }
- }
+ if ( cachedData.borderData ) {
+ const { width: borderWidth, color: borderColor } =
+ cachedData.borderData;
+ const stroke = encodeURIComponent( borderColor || '' );
+ const svg = `<svg width="${ width }" height="${ height }" viewBox="0 0 ${ width } ${ height }" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="${ path }" stroke="${ stroke }" stroke-width="${ borderWidth }"/></svg>`;
+ const encodedSvg = encodeURIComponent( svg );
+ block.style.background = `url('data:image/svg+xml,${ encodedSvg }')`;
+ if ( cachedData.background ) {
+ block.style.background += `, ${ cachedData.background }`;
+ }
+ } else if ( cachedData.background ) {
+ block.style.background = cachedData.background;
+ } else {
+ block.style.removeProperty( 'background' );
+ }🤖 Prompt for AI Agents
In dynamic-shapes/src/js/view.js around lines 166 to 176, the SVG background
uses raw borderColor which may contain '#' and breaks the data: URI and old SVG
backgrounds are left when borderData becomes falsy; fix by encoding the SVG (or
at minimum the borderColor) with encodeURIComponent before embedding in the data
URL (e.g., build the svg string then run encodeURIComponent(svg) when creating
the data:image/svg+xml,...) and when cachedData.borderData is falsy explicitly
reset block.style.background to cachedData.background || '' (so previous SVG is
removed) or set it to the cachedData.background only if present.
|
@philcable, is there a zip or link to spin up a test? @nate-allen, I wonder if you could give this a review, or I could spin up an engineering request. Let me know. |
|
@dhanson-wp Sure, here's a zip! dynamic-shapes.zip |
@philcable, can we update the plugin description that appears on the Plugins page?
|
|
@dhanson-wp Sorry about that, I made the zip yesterday before I caught that. It is in fact updated in the PR here: https://github.com/a8cteam51/special-projects-blocks-monorepo/pull/109/files#diff-761c6167e7366e2d22808389130fe750ff1619d28bf50ce19ad6ed29319ddee4R4 |

An abstraction of a feature from another project.
Pinging @dhanson-wp as an interested party :) - sorry for the wait.
I appreciate that this is a lot of code, so please don't hesitate to let me know if there's any way I can be of help during the review process.
Summary by CodeRabbit
New Features
Documentation
Chores