Skip to content

Conversation

@ghengeveld
Copy link
Member

@ghengeveld ghengeveld commented Dec 16, 2025

Contributes to #33245
Telescopes on #33290

What I did

This is an almost complete rewrite of the Zoom tool. Notable changes:

  • Tool is moved from the left-hand side of the toolbar to the right-hand side.
  • The three toolbar items have been replaced with a single button, showing the current zoom percentage.
  • Zooming now uses a set of predefined levels rather than a zoom factor (multiplier), for more predictability and consistency with other software (e.g. DevTools).
  • Keyboard shortcuts ⌥+, ⌥- and ⌥0 have been added for quick access.
  • Custom zoom percentages are now supported, even outside of the usual 25-400% range.
  • The Viewports feature has been updated to play well with zooming, so that the canvas scales along with the zoom level.
Screen.Recording.2026-01-08.at.11.27.01.mov

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • New Features

    • NumericInput with keyboard increment/decrement and unit handling
    • Shortcut badge for displaying keyboard shortcuts
    • Redesigned Zoom tool with numeric input, presets (50/100/200%) and shortcuts
    • Viewport size controls and improved drag handles
  • Refactor

    • Toolbar reorganized so zoom appears in the secondary tools group
    • Shortcut display extracted to a reusable component
  • Tests

    • Storybook stories added for NumericInput and Zoom
  • Improvements

    • Preview iframe positioning and inner scaling adjusted for more consistent viewport rendering

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

@nx-cloud
Copy link

nx-cloud bot commented Dec 16, 2025

View your CI Pipeline Execution ↗ for commit 59eee1c

Command Status Duration Result
nx run-many -t compile,check,knip,test,pretty-d... ✅ Succeeded 10m 38s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-09 16:34:17 UTC

@coderabbitai

This comment was marked as spam.

coderabbitai[bot]

This comment was marked as spam.

@storybook-app-bot
Copy link

storybook-app-bot bot commented Dec 16, 2025

Package Benchmarks

Commit: 59eee1c, ran on 9 January 2026 at 16:36:51 UTC

The following packages have significant changes to their size or dependencies:

@storybook/nextjs

Before After Difference
Dependency count 534 538 🚨 +4 🚨
Self size 646 KB 646 KB 0 B
Dependency size 59.16 MB 59.19 MB 🚨 +28 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-webpack5

Before After Difference
Dependency count 274 278 🚨 +4 🚨
Self size 24 KB 24 KB 0 B
Dependency size 44.09 MB 44.12 MB 🚨 +28 KB 🚨
Bundle Size Analyzer Link Link

@storybook/preset-create-react-app

Before After Difference
Dependency count 64 68 🚨 +4 🚨
Self size 32 KB 32 KB 🚨 +36 B 🚨
Dependency size 5.95 MB 5.98 MB 🚨 +28 KB 🚨
Bundle Size Analyzer Link Link

@storybook/preset-react-webpack

Before After Difference
Dependency count 166 170 🚨 +4 🚨
Self size 18 KB 18 KB 🚨 +52 B 🚨
Dependency size 31.23 MB 31.26 MB 🚨 +28 KB 🚨
Bundle Size Analyzer Link Link

@github-actions github-actions bot added the Stale label Jan 7, 2026
@ghengeveld ghengeveld removed the Stale label Jan 7, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @code/core/src/manager/components/preview/Viewport.tsx:
- Around line 274-286: frameStyles can produce invalid CSS when scale is zero or
negative (resulting in Infinity% or negative sizes); in the frameStyles useMemo
clamp the incoming scale to a safe minimum (e.g., minScale = 0.01 or the same
lower bound used by the zoom controls) before computing width/height so
parseNumber(width/height) times clampedScale is always finite and positive;
reference the parseNumber function and the frameStyles useMemo and apply the
clamp to the local scale variable derived from the scale prop (or also add
equivalent validation in the manual zoom handler in zoom.tsx to reject/normalize
<=0 values).
🧹 Nitpick comments (1)
code/core/src/manager/components/preview/Viewport.tsx (1)

366-366: Consider consistent unit handling for data-value attributes.

The data-value attributes currently use .replace('px', '') which only strips pixel units. If dimensions use other units (%, em, rem), they would remain in the displayed value, creating inconsistency with the numeric-only values shown during drag.

♻️ Proposed refactor for consistent unit stripping
+const stripUnit = (value: string) => parseNumber(value)?.number.toString() ?? value;
+
 <DragHandle
   ref={dragRefX}
   isDefault={isDefault}
   data-side="right"
-  data-value={width.replace('px', '')}
+  data-value={stripUnit(width)}
 />
 <DragHandle
   ref={dragRefY}
   isDefault={isDefault}
   data-side="bottom"
-  data-value={height.replace('px', '')}
+  data-value={stripUnit(height)}
 />

Also applies to: 372-372

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 955f23b and 821c6e1.

📒 Files selected for processing (5)
  • code/.storybook/bench/bench.stories.tsx
  • code/core/src/components/components/ActionList/ActionList.stories.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
  • code/core/src/manager/components/preview/tools/share.tsx
  • code/core/src/manager/components/sidebar/ContextMenu.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • code/core/src/components/components/ActionList/ActionList.stories.tsx
  • code/.storybook/bench/bench.stories.tsx
  • code/core/src/manager/components/sidebar/ContextMenu.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,ts,tsx,json,md,html,css,scss}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Format code using Prettier with yarn prettier --write <file>

Files:

  • code/core/src/manager/components/preview/tools/share.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run ESLint checks using yarn lint:js:cmd <file> or the full command cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives to fix linting errors before committing

Files:

  • code/core/src/manager/components/preview/tools/share.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode across all packages

Files:

  • code/core/src/manager/components/preview/tools/share.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Export functions from modules if they need to be tested
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/manager/components/preview/tools/share.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
code/{core,lib,addons,builders,frameworks,presets}/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use logger from storybook/internal/node-logger for server-side logging in Node.js code

Files:

  • code/core/src/manager/components/preview/tools/share.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
🧠 Learnings (4)
📓 Common learnings
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/tooltip/WithTooltip.tsx:54-96
Timestamp: 2025-11-05T09:37:25.920Z
Learning: Repo: storybookjs/storybook — In code/core/src/components/components/tooltip/WithTooltip.tsx, the legacy WithTooltip implementation is intentionally reintroduced for backward compatibility and is deprecated; maintainers (per Sidnioulz) do not want maintenance or improvements on it. Prefer WithTooltipNew/Popover; avoid suggesting changes to WithTooltip.* going forward.
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32507
File: code/core/src/manager/globals/globals-module-info.ts:25-33
Timestamp: 2025-09-24T09:39:39.233Z
Learning: In Storybook, storybook/actions/decorator is a preview-only entrypoint and should not be included in manager globals configuration. The duplicatedKeys array in code/core/src/manager/globals/globals-module-info.ts is specifically for manager-side externalization, not preview entrypoints.
📚 Learning: 2025-09-24T09:39:39.233Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32507
File: code/core/src/manager/globals/globals-module-info.ts:25-33
Timestamp: 2025-09-24T09:39:39.233Z
Learning: In Storybook, storybook/actions/decorator is a preview-only entrypoint and should not be included in manager globals configuration. The duplicatedKeys array in code/core/src/manager/globals/globals-module-info.ts is specifically for manager-side externalization, not preview entrypoints.

Applied to files:

  • code/core/src/manager/components/preview/tools/share.tsx
📚 Learning: 2025-11-05T09:37:25.920Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/tooltip/WithTooltip.tsx:54-96
Timestamp: 2025-11-05T09:37:25.920Z
Learning: Repo: storybookjs/storybook — In code/core/src/components/components/tooltip/WithTooltip.tsx, the legacy WithTooltip implementation is intentionally reintroduced for backward compatibility and is deprecated; maintainers (per Sidnioulz) do not want maintenance or improvements on it. Prefer WithTooltipNew/Popover; avoid suggesting changes to WithTooltip.* going forward.

Applied to files:

  • code/core/src/manager/components/preview/tools/share.tsx
📚 Learning: 2025-11-05T09:38:47.712Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.

Applied to files:

  • code/core/src/manager/components/preview/Viewport.tsx
🧬 Code graph analysis (1)
code/core/src/manager/components/preview/Viewport.tsx (2)
code/core/src/manager/components/preview/NumericInput.tsx (1)
  • NumericInput (66-152)
code/core/src/manager/components/preview/Iframe.tsx (1)
  • IFrame (29-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: normal
  • GitHub Check: nx
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (8)
code/core/src/manager/components/preview/tools/share.tsx (1)

15-15: LGTM! Import path updated to use centralized Shortcut component.

The import path change is correct and aligns with the refactoring effort to extract Shortcut into a standalone, shared component.

code/core/src/manager/components/preview/Viewport.tsx (7)

1-1: LGTM: Import updates support the refactoring.

The addition of useMemo and the switch from SizeInput to NumericInput align well with the performance optimizations and UI component updates in this PR.

Also applies to: 15-15


117-173: LGTM: DragHandle refactor adds helpful dimension feedback.

The conditional rendering based on isDefault and the addition of data-value display via ::after pseudo-elements provide good visual feedback during resizing. The cursor types and positioning are appropriate for each drag direction.


175-192: LGTM: ScrollEdge provides smooth drag experience.

The ScrollEdge components are cleverly positioned outside the frame to serve as scroll anchors, ensuring drag handles remain visible during resize operations. The pointerEvents: 'none' ensures they don't interfere with user interaction.


194-198: LGTM: Clean component composition.

The SizeInput styled wrapper provides consistent dimensions for the numeric input controls while keeping the underlying NumericInput component generic and reusable.


223-272: LGTM: Enhanced drag behavior with scroll-into-view.

The drag logic correctly updates dimension feedback via data-value attributes and uses scrollIntoView to keep handles visible during resize. The onEnd handler properly compensates for the scale factor by dividing clientWidth/clientHeight by scale before calling resize.


296-300: LGTM: Consistent UI pattern with ActionList.

Using ActionList.Action for the W/H prefixes aligns with the toolbar's component patterns. The aria-hidden attribute is appropriate since the SizeInput components have their own labels for accessibility.

Also applies to: 316-320


341-361: LGTM: CSS transform-based scaling implementation.

The scaling approach is sound: the FrameWrapper is sized to the scaled dimensions, the inner div is inversely sized and then scaled up via CSS transform, and the IFrame receives scale={1} since scaling is handled by the wrapper. This allows the viewport content to scale while keeping UI elements (handles, edges) properly positioned.

The logic is complex but necessary to support the zoom feature while maintaining interactive resize handles.

…ions

- Updated ZoomIFrame to correctly apply scaling transformations.
- Enhanced ZoomProvider to manage zoom levels with predefined constants.
- Refactored Zoom component to utilize ActionList for zoom controls and added keyboard shortcuts for zoom actions.
- Improved ZoomWrapper to dynamically adjust zoom levels based on user interactions.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @code/core/src/components/components/Zoom/ZoomIFrame.tsx:
- Around line 42-44: ZoomIFrame currently uses @ts-expect-error to silence
strict TypeScript issues for the iframe property and related refs and DOM
access; replace those suppressions with proper typing and null checks: type the
component's iframe prop and iFrameRef as HTMLIFrameElement | null (or
React.RefObject<HTMLIFrameElement>), guard accesses to iFrameRef.current and
iFrameRef.current.contentDocument with nullish checks before assigning or
reading body.style, and narrow types when setting style (e.g., ensure values are
strings like `${(1/scale)*100}%`); also add unit tests for ZoomIFrame rendering
at scales 0.5, 1, and 2 to verify computed width/height and transform values.

In @code/core/src/manager/components/preview/NumericInput.tsx:
- Around line 91-99: The cursor positioning in setInputSelection incorrectly
skips when index === 0; change the condition so it triggers for any non-negative
index (e.g., use index >= 0 or index !== -1) and still guard input exists before
calling input.setSelectionRange(index, index). Update the useCallback block that
references inputRef.current and the computed index (const index =
input?.value.search(/[^\d.]/) ?? -1) to check for index >= 0 (or index !== -1)
instead of index > 0 so the cursor is set at the start of the field as well.
- Around line 66-77: The ref type declared in forwardRef for NumericInput ({
select: () => void }) does not match what useImperativeHandle returns
(inputRef.current, an HTMLInputElement); fix by making useImperativeHandle
expose an object that matches the declared shape: call
useImperativeHandle(forwardedRef, () => ({ select: () =>
inputRef.current?.select?.() })), ensuring you reference inputRef and
forwardedRef; alternatively, if you prefer exposing the raw element, change the
forwardRef generic from { select: () => void } to HTMLInputElement so the types
align.

In @code/core/src/manager/components/preview/tools/zoom.tsx:
- Around line 168-173: The zoomOut callback uses Array.prototype.findLast
(ES2023) which breaks ES2020 targets; replace it with an ES2020-compatible
search over ZOOM_LEVELS (e.g., iterate from the end and pick the first level <
value) inside the zoomOut function and then call set(lowerZoomLevel) when found;
update references to zoomOut, ZOOM_LEVELS, set, and value accordingly so
behavior remains the same without using findLast.

In @code/core/src/manager/components/preview/Viewport.tsx:
- Around line 345-351: Compute a safeScale and use it instead of raw scale to
avoid Infinity/negative CSS values: e.g. derive safeScale = (typeof scale ===
'number' && isFinite(scale) && scale > 0) ? scale : 1 (or clamp with
Math.max(scale, 0.01) if you prefer a minimum zoom), then replace the direct
uses of scale in the inline style block (height, width, transform) and in
frameStyles with safeScale so layout never receives 0, negative, NaN or Infinity
values.
🧹 Nitpick comments (4)
code/core/src/manager/components/preview/Viewport.tsx (2)

274-277: Consider handling edge cases in parseNumber.

The regex doesn't match negative values or values with whitespace (e.g., "100 px"). If callers ever pass such values, parseNumber returns undefined, falling back to { number: 0, unit: 'px' }, which silently produces zero-sized dimensions.

If negative or whitespace-padded inputs are possible, consider:

♻️ More robust regex (optional)
 const parseNumber = (value: string) => {
-  const [match, number, unit] = value.match(/^(\d+(?:\.\d+)?)(\%|[a-z]{0,4})?$/) || [];
+  const [match, number, unit] = value.trim().match(/^(-?\d+(?:\.\d+)?)(\%|[a-z]{0,4})?$/) || [];
   return match ? { number: Number(number), unit } : undefined;
 };

362-373: Consider using parseNumber for consistent data-value extraction.

The .replace('px', '') only handles px units. If width/height uses %, em, or other units, the data-value will include the unit string. For consistency, consider reusing parseNumber:

♻️ Consistent unit handling
 <DragHandle
   ref={dragRefX}
   isDefault={isDefault}
   data-side="right"
-  data-value={width.replace('px', '')}
+  data-value={parseNumber(width)?.number ?? width}
 />
 <DragHandle
   ref={dragRefY}
   isDefault={isDefault}
   data-side="bottom"
-  data-value={height.replace('px', '')}
+  data-value={parseNumber(height)?.number ?? height}
 />
code/core/src/manager/components/preview/NumericInput.tsx (1)

112-134: Clarify unit handling logic in arrow key handler.

The arrow key handler has inconsistent unit handling:

  • Line 123: setInputValue strips baseUnit from the display value
  • Line 124: setValue includes the unit in the external value

This seems intentional (display without baseUnit, store with baseUnit), but the logic is subtle. Consider adding a comment explaining why baseUnit is stripped for display but included in the stored value.

📝 Suggested documentation
         if (!Number.isNaN(num) && update >= 0) {
           const inputUnit =
             inputValue.match(/(\d+(?:\.\d+)?)(\%|[a-z]{0,4})?$/)?.[2] || unit || baseUnit || '';
+          // Strip baseUnit from display (user sees "100") but include in stored value ("100px")
           setInputValue(`${update}${inputUnit === baseUnit ? '' : inputUnit}`);
           setValue(`${update}${inputUnit}`);
           setInputSelection();
         }
code/core/src/manager/components/preview/tools/zoom.tsx (1)

182-201: Consider optimizing shortcut re-registration.

The shortcuts are re-registered on every value change because zoomIn and zoomOut are recreated when value changes. While functional, this could be optimized by using refs or restructuring the callbacks.

Potential optimization approach

Consider using a ref to hold the current value and create stable callbacks:

  const zoomIn = useCallback(() => {
-    const higherZoomLevel = ZOOM_LEVELS.find((level) => level > value);
+    const higherZoomLevel = ZOOM_LEVELS.find((level) => level > valueRef.current);
    if (higherZoomLevel) {
      set(higherZoomLevel);
    }
-  }, [set, value]);
+  }, [set]);

This would prevent re-registration of shortcuts on every zoom change.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db7ff3d and 51890fc.

📒 Files selected for processing (16)
  • code/.storybook/bench/bench.stories.tsx
  • code/core/src/components/components/ActionList/ActionList.stories.tsx
  • code/core/src/components/components/Zoom/ZoomIFrame.tsx
  • code/core/src/manager-api/modules/shortcuts.ts
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/manager/components/preview/NumericInput.stories.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/SizeInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
  • code/core/src/manager/components/preview/tools/share.tsx
  • code/core/src/manager/components/preview/tools/zoom.stories.tsx
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/components/sidebar/ContextMenu.tsx
  • code/core/src/manager/container/Menu.stories.tsx
  • code/core/src/manager/container/Menu.tsx
  • code/core/src/manager/container/Preview.tsx
💤 Files with no reviewable changes (1)
  • code/core/src/manager/components/preview/SizeInput.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • code/core/src/manager/components/sidebar/ContextMenu.tsx
  • code/.storybook/bench/bench.stories.tsx
  • code/core/src/manager/components/preview/tools/zoom.stories.tsx
  • code/core/src/manager/components/preview/NumericInput.stories.tsx
  • code/core/src/manager/container/Menu.stories.tsx
  • code/core/src/manager/container/Preview.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,ts,tsx,json,md,html,css,scss}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Format code using Prettier with yarn prettier --write <file>

Files:

  • code/core/src/manager-api/modules/shortcuts.ts
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/container/Menu.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
  • code/core/src/components/components/ActionList/ActionList.stories.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/components/components/Zoom/ZoomIFrame.tsx
  • code/core/src/manager/components/preview/tools/share.tsx
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run ESLint checks using yarn lint:js:cmd <file> or the full command cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives to fix linting errors before committing

Files:

  • code/core/src/manager-api/modules/shortcuts.ts
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/container/Menu.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
  • code/core/src/components/components/ActionList/ActionList.stories.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/components/components/Zoom/ZoomIFrame.tsx
  • code/core/src/manager/components/preview/tools/share.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode across all packages

Files:

  • code/core/src/manager-api/modules/shortcuts.ts
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/container/Menu.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
  • code/core/src/components/components/ActionList/ActionList.stories.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/components/components/Zoom/ZoomIFrame.tsx
  • code/core/src/manager/components/preview/tools/share.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Export functions from modules if they need to be tested
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/manager-api/modules/shortcuts.ts
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/container/Menu.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
  • code/core/src/components/components/ActionList/ActionList.stories.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/components/components/Zoom/ZoomIFrame.tsx
  • code/core/src/manager/components/preview/tools/share.tsx
code/{core,lib,addons,builders,frameworks,presets}/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use logger from storybook/internal/node-logger for server-side logging in Node.js code

Files:

  • code/core/src/manager-api/modules/shortcuts.ts
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/container/Menu.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
  • code/core/src/components/components/ActionList/ActionList.stories.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/components/components/Zoom/ZoomIFrame.tsx
  • code/core/src/manager/components/preview/tools/share.tsx
🧠 Learnings (4)
📚 Learning: 2025-11-05T09:37:25.920Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/tooltip/WithTooltip.tsx:54-96
Timestamp: 2025-11-05T09:37:25.920Z
Learning: Repo: storybookjs/storybook — In code/core/src/components/components/tooltip/WithTooltip.tsx, the legacy WithTooltip implementation is intentionally reintroduced for backward compatibility and is deprecated; maintainers (per Sidnioulz) do not want maintenance or improvements on it. Prefer WithTooltipNew/Popover; avoid suggesting changes to WithTooltip.* going forward.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/container/Menu.tsx
  • code/core/src/manager/components/preview/tools/share.tsx
📚 Learning: 2025-11-05T09:38:47.712Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/container/Menu.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
📚 Learning: 2025-09-24T09:39:39.233Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32507
File: code/core/src/manager/globals/globals-module-info.ts:25-33
Timestamp: 2025-09-24T09:39:39.233Z
Learning: In Storybook, storybook/actions/decorator is a preview-only entrypoint and should not be included in manager globals configuration. The duplicatedKeys array in code/core/src/manager/globals/globals-module-info.ts is specifically for manager-side externalization, not preview entrypoints.

Applied to files:

  • code/core/src/manager/container/Menu.tsx
  • code/core/src/components/components/ActionList/ActionList.stories.tsx
  • code/core/src/manager/components/preview/tools/share.tsx
📚 Learning: 2025-10-03T07:55:42.639Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/manager/components/preview/Toolbar.tsx:102-105
Timestamp: 2025-10-03T07:55:42.639Z
Learning: In code/core/src/manager/components/preview/Toolbar.tsx, we intentionally do not add aria-label/aria-labelledby to StyledToolbar (AbstractToolbar) because the enclosing section is already labeled via an sr-only heading and the toolbar is the only content. Revisit only if real user testing indicates a need.

Applied to files:

  • code/core/src/manager/components/preview/tools/share.tsx
🧬 Code graph analysis (3)
code/core/src/manager/components/preview/tools/zoom.tsx (2)
code/core/src/manager/components/preview/NumericInput.tsx (1)
  • NumericInput (66-152)
code/core/src/manager-api/root.tsx (1)
  • useStorybookApi (296-299)
code/core/src/manager/components/preview/NumericInput.tsx (2)
code/core/src/components/index.ts (1)
  • Form (62-62)
code/core/src/preview-api/index.ts (5)
  • useMemo (10-10)
  • useRef (13-13)
  • useState (14-14)
  • useCallback (6-6)
  • useEffect (8-8)
code/core/src/manager/components/Shortcut.tsx (1)
code/core/src/manager-api/modules/shortcuts.ts (1)
  • keys (23-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
  • GitHub Check: nx
🔇 Additional comments (11)
code/core/src/manager/components/preview/Viewport.tsx (1)

222-271: Event handler implementation looks solid.

The drag handling logic properly:

  • Cleans up event listeners in the effect cleanup and onEnd
  • Uses refs to track drag state across renders
  • Applies the || 1 fallback for scale in onEnd (line 252)
  • Scrolls handles into view during drag for better UX

The non-null assertions on refs are safe here since the handlers only fire after mount.

code/core/src/components/components/ActionList/ActionList.stories.tsx (1)

5-5: LGTM! Import path correctly updated.

The import path has been updated to reference the newly extracted Shortcut component. This aligns with the PR's refactoring effort to centralize the Shortcut component for reuse across the codebase.

code/core/src/manager-api/modules/shortcuts.ts (2)

48-50: Excellent functional update pattern.

The updated signature allows setShortcuts to accept either a value or an updater function, following React's setState pattern. This prevents race conditions when multiple shortcut updates occur in quick succession and ensures updates are always based on the latest state.

Also applies to: 210-216


221-221: Consistent usage of updater pattern.

Both setShortcut and setAddonShortcut correctly use the new functional update pattern, ensuring immutable state updates and consistency across the API.

Also applies to: 225-228

code/core/src/manager/components/preview/tools/share.tsx (1)

15-15: LGTM! Import path correctly updated.

The import path has been updated to reference the centralized Shortcut component, consistent with the refactoring across the codebase.

code/core/src/manager/components/Shortcut.tsx (1)

1-36: LGTM! Clean component extraction.

The Shortcut component is well-implemented and successfully centralizes shortcut rendering. The styling is consistent with Storybook's design system, and the logic is straightforward.

code/core/src/manager/container/Menu.tsx (1)

20-20: LGTM! Successful migration to centralized Shortcut component.

The import change correctly references the extracted Shortcut component, and all usages throughout the file remain compatible.

code/core/src/manager/components/preview/tools/zoom.tsx (4)

1-16: LGTM! Well-defined zoom levels.

The predefined ZOOM_LEVELS array provides good coverage from 25% to 400%, and the constants are clearly named.


32-52: LGTM! Correct context provider implementation.

The ZoomProvider properly manages zoom state and conditionally applies scaling based on the shouldScale prop, which aligns with the PR's goal of making the canvas scale with zoom level.


54-153: LGTM! Comprehensive zoom UI with good UX.

The popover-based UI provides intuitive controls with manual input, preset options, and keyboard shortcuts. The focus management (lines 65-69) enhances usability by auto-selecting the input when the popover opens.


206-212: LGTM! Proper addon registration.

The zoom tool is correctly registered with appropriate match conditions and uses the context consumer pattern to access zoom state.

Comment on lines +168 to +173
const zoomOut = useCallback(() => {
const lowerZoomLevel = ZOOM_LEVELS.findLast((level) => level < value);
if (lowerZoomLevel) {
set(lowerZoomLevel);
}
}, [set, value]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check all usages of findLast in the codebase
rg -n "\.findLast\(" --type ts --type tsx -C 2

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

# Check the tsconfig and build configuration to understand target browser support
find . -name "tsconfig*.json" -o -name "*.config.js" -o -name "*.config.ts" | grep -E "(tsconfig|webpack|babel|vite)" | head -10

Repository: storybookjs/storybook

Length of output: 441


🏁 Script executed:

# Check the specific file context and surrounding code
head -180 code/core/src/manager/components/preview/tools/zoom.tsx | tail -20

Repository: storybookjs/storybook

Length of output: 543


🏁 Script executed:

# Search for any polyfill configurations or ES2023 support mentions
rg -i "findlast|polyfill|es2023|es2022" --type json --type ts --type tsx | head -20

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

# Check package.json for browserslist or browser support target
cat package.json | grep -A 10 -B 2 "browserslist\|engines\|target"

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Search for findLast usage with corrected syntax
rg "findLast" --type ts -C 2

Repository: storybookjs/storybook

Length of output: 2167


🏁 Script executed:

# Check the main tsconfig to understand target browser support
cat code/tsconfig.base.json 2>/dev/null || cat tsconfig.json 2>/dev/null || cat code/core/tsconfig.json

Repository: storybookjs/storybook

Length of output: 338


🏁 Script executed:

# Look for browserslist or engine configuration
cat package.json | jq '.browserslist, .engines'

Repository: storybookjs/storybook

Length of output: 74


🏁 Script executed:

# Search for any polyfill or ES2023 compatibility mentions in config files
rg "target|lib|ES2023|ES2022" code/tsconfig.base.json code/core/tsconfig.json --type json 2>/dev/null || find code -name "tsconfig.json" -exec grep -H "target\|lib" {} \; | head -20

Repository: storybookjs/storybook

Length of output: 2685


🏁 Script executed:

# Count how many times findLast appears in the codebase
git grep "findLast" | wc -l

Repository: storybookjs/storybook

Length of output: 66


🏁 Script executed:

# Check the root tsconfig
cat tsconfig.json

Repository: storybookjs/storybook

Length of output: 110


🏁 Script executed:

# Check if there's a specific target mentioned in any build or webpack config
find . -maxdepth 2 -name "*.config.*" -type f | head -20

Repository: storybookjs/storybook

Length of output: 309


🏁 Script executed:

# Check the package.json for any relevant metadata about browser support
cat package.json | grep -A 5 -B 5 "browserslist\|target\|esnext" | head -30

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Look at the actual project structure for where zoom.tsx is compiled
cat code/core/tsconfig.json

Repository: storybookjs/storybook

Length of output: 338


🏁 Script executed:

# Check the parent tsconfig that code/core/tsconfig.json extends
cat code/tsconfig.json

Repository: storybookjs/storybook

Length of output: 723


🏁 Script executed:

# Look for any build configuration that might transpile ES2023 features
rg "es2023|es2022|findLast" code --type json -C 2

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Check if there's any .babelrc or similar transpilation config
find code -maxdepth 3 -name ".babelrc*" -o -name "babel.config.*" -o -name "webpack.config.*" | head -10

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Check the actual browser support target in package.json more comprehensively
head -50 package.json

Repository: storybookjs/storybook

Length of output: 2477


🏁 Script executed:

# Check if there's any webpack or build configuration that might handle transpilation
find code/core -name "*.config.*" -o -name "webpack*" -o -name "vite*" | head -20

Repository: storybookjs/storybook

Length of output: 190


🏁 Script executed:

# Check the git log for zoom.tsx to see when findLast was introduced
git log --oneline -n 20 code/core/src/manager/components/preview/tools/zoom.tsx

Repository: storybookjs/storybook

Length of output: 85


🏁 Script executed:

# Check if there are any build-time transformations mentioned in package.json scripts
cat package.json | grep -A 50 '"scripts"' | grep -E "build|compile|transpile"

Repository: storybookjs/storybook

Length of output: 276


🏁 Script executed:

# Verify what browsers/runtime Storybook officially targets
rg "browser|target|support" code/core/package.json --type json -A 2 -B 2

Repository: storybookjs/storybook

Length of output: 365


🏁 Script executed:

# Check if there's a polyfill or shim being applied at runtime
rg "findLast|Array.prototype" code/core/src --type ts -B 3 -A 3

Repository: storybookjs/storybook

Length of output: 649


🏁 Script executed:

# Check the core/package.json specifically for build configuration and dependencies
cat code/core/package.json | head -100

Repository: storybookjs/storybook

Length of output: 2947


🏁 Script executed:

# Look for any mention of ES compatibility or version support in docs or config
rg "ES2023|ES2022|browser.*support" code --type md -C 2 | head -40

Repository: storybookjs/storybook

Length of output: 47


Use a fallback implementation compatible with ES2020.

The findLast method is an ES2023 feature. Since Storybook targets ES2020, this will fail in browsers without ES2023 support.

Suggested fix
  const zoomOut = useCallback(() => {
-    const lowerZoomLevel = ZOOM_LEVELS.findLast((level) => level < value);
+    const lowerZoomLevel = [...ZOOM_LEVELS].reverse().find((level) => level < value);
    if (lowerZoomLevel) {
      set(lowerZoomLevel);
    }
  }, [set, value]);
📝 Committable suggestion

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

Suggested change
const zoomOut = useCallback(() => {
const lowerZoomLevel = ZOOM_LEVELS.findLast((level) => level < value);
if (lowerZoomLevel) {
set(lowerZoomLevel);
}
}, [set, value]);
const zoomOut = useCallback(() => {
const lowerZoomLevel = [...ZOOM_LEVELS].reverse().find((level) => level < value);
if (lowerZoomLevel) {
set(lowerZoomLevel);
}
}, [set, value]);
🤖 Prompt for AI Agents
In @code/core/src/manager/components/preview/tools/zoom.tsx around lines 168 -
173, The zoomOut callback uses Array.prototype.findLast (ES2023) which breaks
ES2020 targets; replace it with an ES2020-compatible search over ZOOM_LEVELS
(e.g., iterate from the end and pick the first level < value) inside the zoomOut
function and then call set(lowerZoomLevel) when found; update references to
zoomOut, ZOOM_LEVELS, set, and value accordingly so behavior remains the same
without using findLast.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
code/core/src/manager-api/tests/shortcuts.test.js (1)

9-12: Add test coverage for the functional update branch.

The mock now supports functional updates (line 10), but no test exercises this new capability. While the shortcuts module may use functional updates internally, the mock's conditional logic remains untested.

🧪 Suggested test case

Consider adding a test that directly verifies the functional update pattern:

it('supports functional updates in setState', () => {
  const store = createMockStore();
  store.setState({ shortcuts: { fullScreen: ['alt', 'F'] } });
  
  const result = store.setState((currentState) => ({
    shortcuts: { ...currentState.shortcuts, fullScreen: ['X'] }
  }));
  
  expect(result.shortcuts.fullScreen).toEqual(['X']);
  expect(store.getState().shortcuts.fullScreen).toEqual(['X']);
});

As per coding guidelines, tests should cover all branches and variations, including the new typeof update === 'function' path.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77a8272 and 25cf339.

📒 Files selected for processing (1)
  • code/core/src/manager-api/tests/shortcuts.test.js
🧰 Additional context used
📓 Path-based instructions (6)
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/spy-mocking.mdc)

**/*.test.{ts,tsx,js,jsx}: Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests
Place all mocks at the top of the test file before any test cases
Use vi.mocked() to type and access the mocked functions in Vitest tests
Implement mock behaviors in beforeEach blocks in Vitest tests
Mock all required dependencies that the test subject uses
Each mock implementation should return a Promise for async functions in Vitest
Mock implementations should match the expected return type of the original function
Mock all required properties and methods that the test subject uses in Vitest tests
Avoid direct function mocking without vi.mocked() in Vitest tests
Avoid mock implementations outside of beforeEach blocks in Vitest tests
Avoid mocking without the spy: true option in Vitest tests
Avoid inline mock implementations within test cases in Vitest tests
Avoid mocking only a subset of required dependencies in Vitest tests
Mock at the highest level of abstraction needed in Vitest tests
Keep mock implementations simple and focused in Vitest tests
Use type-safe mocking with vi.mocked() in Vitest tests
Document complex mock behaviors in Vitest tests
Group related mocks together in Vitest tests

Files:

  • code/core/src/manager-api/tests/shortcuts.test.js
**/*.{js,jsx,ts,tsx,json,md,html,css,scss}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Format code using Prettier with yarn prettier --write <file>

Files:

  • code/core/src/manager-api/tests/shortcuts.test.js
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run ESLint checks using yarn lint:js:cmd <file> or the full command cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives to fix linting errors before committing

Files:

  • code/core/src/manager-api/tests/shortcuts.test.js
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Export functions from modules if they need to be tested
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/manager-api/tests/shortcuts.test.js
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{test,spec}.{ts,tsx,js,jsx}: Write meaningful unit tests that actually import and call the functions being tested, not just verify syntax patterns
Achieve high test coverage of business logic, aiming for 75%+ coverage of statements/lines
Cover all branches, conditions, edge cases, error paths, and different input variations in unit tests
Use vi.mock() to mock file system, loggers, and other external dependencies in tests

Files:

  • code/core/src/manager-api/tests/shortcuts.test.js
code/{core,lib,addons,builders,frameworks,presets}/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use logger from storybook/internal/node-logger for server-side logging in Node.js code

Files:

  • code/core/src/manager-api/tests/shortcuts.test.js
🧠 Learnings (10)
📚 Learning: 2025-12-22T22:03:40.123Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-22T22:03:40.123Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Use `vi.mock()` to mock file system, loggers, and other external dependencies in tests

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mocked()` to type and access the mocked functions in Vitest tests

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use `vi.mock()` with the `spy: true` option for all package and file mocks in Vitest tests

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use type-safe mocking with `vi.mocked()` in Vitest tests

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Mock all required properties and methods that the test subject uses in Vitest tests

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Each mock implementation should return a Promise for async functions in Vitest

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid direct function mocking without `vi.mocked()` in Vitest tests

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Document complex mock behaviors in Vitest tests

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Implement mock behaviors in `beforeEach` blocks in Vitest tests

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Keep mock implementations simple and focused in Vitest tests

Applied to files:

  • code/core/src/manager-api/tests/shortcuts.test.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
  • GitHub Check: nx

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
code/core/src/manager/components/preview/tools/zoom.tsx (1)

136-138: Inconsistent ActionList.Text wrapping.

The 200% action text is not wrapped in ActionList.Text, unlike the other action items (lines 110, 120, 126, 131). For consistency, wrap it in ActionList.Text.

♻️ Proposed fix
 <ActionList.Action onClick={() => zoomTo(2)} ariaLabel="Zoom to 200%">
-  200%
+  <ActionList.Text>200%</ActionList.Text>
 </ActionList.Action>
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25cf339 and ca968fd.

📒 Files selected for processing (2)
  • code/core/src/manager/components/preview/Viewport.tsx
  • code/core/src/manager/components/preview/tools/zoom.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/src/manager/components/preview/Viewport.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,ts,tsx,json,md,html,css,scss}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Format code using Prettier with yarn prettier --write <file>

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run ESLint checks using yarn lint:js:cmd <file> or the full command cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives to fix linting errors before committing

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode across all packages

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Export functions from modules if they need to be tested
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
code/{core,lib,addons,builders,frameworks,presets}/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use logger from storybook/internal/node-logger for server-side logging in Node.js code

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
🧠 Learnings (2)
📚 Learning: 2025-11-05T09:37:25.920Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/tooltip/WithTooltip.tsx:54-96
Timestamp: 2025-11-05T09:37:25.920Z
Learning: Repo: storybookjs/storybook — In code/core/src/components/components/tooltip/WithTooltip.tsx, the legacy WithTooltip implementation is intentionally reintroduced for backward compatibility and is deprecated; maintainers (per Sidnioulz) do not want maintenance or improvements on it. Prefer WithTooltipNew/Popover; avoid suggesting changes to WithTooltip.* going forward.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-11-05T09:38:47.712Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
🧬 Code graph analysis (1)
code/core/src/manager/components/preview/tools/zoom.tsx (4)
code/core/src/components/components/Button/Button.tsx (1)
  • Button (53-157)
code/core/src/manager/components/preview/NumericInput.tsx (1)
  • NumericInput (66-152)
code/core/src/preview-api/index.ts (3)
  • useRef (13-13)
  • useCallback (6-6)
  • useEffect (8-8)
code/core/src/manager-api/root.tsx (1)
  • useStorybookApi (296-299)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: normal
  • GitHub Check: nx
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (2)
code/core/src/manager/components/preview/tools/zoom.tsx (2)

183-202: Verify consistency between displayed and registered keyboard shortcuts.

Line 111 displays the zoom in shortcut as ['alt', '+'], but line 192 registers it as ['alt', '=']. While these typically map to the same physical key (with = being the unshifted variant), ensure this inconsistency doesn't confuse users or cause the shortcut to fail. Consider aligning the display with the registered key or documenting this intentional difference.

#!/bin/bash
# Search for other shortcut registrations and displays to check the pattern used in the codebase

echo "=== Checking setAddonShortcut usage patterns ==="
rg -n "setAddonShortcut" -A 5 -B 2

echo ""
echo "=== Checking Shortcut component usage patterns ==="
rg -n "<Shortcut keys=" -B 2 -A 2

144-152: Remove deprecated active prop from ZoomButton.

The active prop on Button is deprecated and will be removed in Storybook 11. Replace with ToggleButton or adjust the styling/state management approach to indicate the non-default zoom state without relying on the active prop.

⛔ Skipped due to learnings
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Tabs/Tabs.stories.tsx:222-227
Timestamp: 2025-11-05T09:36:55.944Z
Learning: Repo: storybookjs/storybook PR: 32458 — In code/core/src/components/components/Button/Button.tsx (React/TypeScript), ButtonProps includes ariaLabel?: string | false and the component maps it to the DOM aria-label. Convention: ariaLabel is mandatory on all Button usages — provide a descriptive string for icon-only buttons; set ariaLabel=false when the button’s children already serve as the accessible name. Do not suggest using a raw aria-label prop on Button call sites.
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/tooltip/WithTooltip.tsx:54-96
Timestamp: 2025-11-05T09:37:25.920Z
Learning: Repo: storybookjs/storybook — In code/core/src/components/components/tooltip/WithTooltip.tsx, the legacy WithTooltip implementation is intentionally reintroduced for backward compatibility and is deprecated; maintainers (per Sidnioulz) do not want maintenance or improvements on it. Prefer WithTooltipNew/Popover; avoid suggesting changes to WithTooltip.* going forward.

@ghengeveld ghengeveld requested a review from Sidnioulz January 8, 2026 12:48
Copy link
Member

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

LGTM, all comments minor.

If you're ok with it, I'd like to try something on your branch so that ArrowUp + ArrowDown on the zoom tool cycle through the zoom levels without needing to open the dialog. Many other buttons in the toolbar trigger on ArrowUp + ArrowDown so I wanna allow for users' muscle memory to pay off.

Base automatically changed from viewport-viewer to next January 9, 2026 13:24
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
code/core/src/manager/components/preview/NumericInput.tsx (1)

114-141: Consider stricter unit validation in the regex pattern.

The regex on line 129 (/(\d+(?:\.\d+)?)(\%|[a-z]{0,4})?$/) is quite permissive, accepting any 0-4 letter suffix. While this provides flexibility, it also allows invalid units like "abcd". If the component should only support specific CSS units (px, em, rem, vw, vh, etc.), consider using a more restrictive pattern.

♻️ Optional: Stricter unit validation
-        const inputUnit =
-          inputValue.match(/(\d+(?:\.\d+)?)(\%|[a-z]{0,4})?$/)?.[2] || unit || baseUnit || '';
+        const inputUnit =
+          inputValue.match(/(\d+(?:\.\d+)?)(\%|px|em|rem|vw|vh|vmin|vmax)?$/)?.[2] || unit || baseUnit || '';

This restricts units to common CSS values. Adjust the list based on your requirements.

code/core/src/manager/components/preview/Viewport.tsx (1)

200-203: Consider stricter unit validation (same pattern as NumericInput).

The parseNumber regex uses the same permissive pattern /^(\d+(?:\.\d+)?)(\%|[a-z]{0,4})?$/ that accepts any 0-4 letter suffix. For consistency and type safety, consider using the same stricter unit validation suggested for NumericInput if you decide to implement it.

Note: This is the same pattern used in NumericInput.tsx line 129, so any changes should be coordinated across both files to maintain consistency.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca968fd and 80cbd5b.

📒 Files selected for processing (2)
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,ts,tsx,json,md,html,css,scss}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Format code using Prettier with yarn prettier --write <file>

Files:

  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run ESLint checks using yarn lint:js:cmd <file> or the full command cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives to fix linting errors before committing

Files:

  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode across all packages

Files:

  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Export functions from modules if they need to be tested
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
code/{core,lib,addons,builders,frameworks,presets}/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use logger from storybook/internal/node-logger for server-side logging in Node.js code

Files:

  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-05T09:38:47.712Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.

Applied to files:

  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
🧬 Code graph analysis (1)
code/core/src/manager/components/preview/NumericInput.tsx (2)
code/core/src/components/index.ts (1)
  • Form (62-62)
code/core/src/preview-api/index.ts (5)
  • useMemo (10-10)
  • useRef (13-13)
  • useState (14-14)
  • useCallback (6-6)
  • useEffect (8-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: normal
  • GitHub Check: nx
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (7)
code/core/src/manager/components/preview/NumericInput.tsx (4)

14-14: LGTM: Using React Aria's useId for React 17 compatibility.

The useId from @react-aria/utils provides a compatibility layer that works with React 17, unlike React.useId which requires React 18+. This aligns with the project's need to support React 17 until Storybook 11 is released.

Based on learnings, this is the recommended approach for generating IDs while maintaining React 17 compatibility.


93-101: LGTM: Cursor positioning logic is correct.

The cursor positioning places the selection just before the unit suffix (e.g., between "100.5" and "px"), which provides good UX when users focus the input. The use of requestAnimationFrame ensures the DOM has updated before manipulating selection.


143-163: LGTM: Component structure is well-designed.

The component correctly handles:

  • Screen-reader accessibility with the sb-sr-only label
  • ID generation with fallback to useId
  • Proper focus/blur handling to sync internal and external state
  • Optional before/after adornments for flexible composition

81-91: The onChange handler intentionally allows non-numeric values to support intermediate editing states (e.g., when a user is typing "100"). Both consumers of this component already validate these values before using them: Viewport.tsx validates through regex pattern matching in the resize() function, and zoom.tsx validates with an explicit !Number.isNaN() check before calling downstream functions. No action needed.

code/core/src/manager/components/preview/Viewport.tsx (3)

279-286: LGTM: Scaling implementation is mathematically sound.

The wrapper-based scaling approach elegantly handles zoom:

  1. FrameWrapper is sized to originalDimensions * scale via frameStyles
  2. Inner wrapper is sized to (1 / scale) * 100% to contain the original dimensions
  3. CSS transform: scale(scale) applies the visual zoom
  4. IFrame always renders at scale={1}, avoiding nested scaling issues

This ensures the iframe content remains at 1:1 pixel ratio while achieving the desired zoom effect, which should prevent rendering artifacts and maintain proper event coordinate handling.

Also applies to: 343-363


228-230: LGTM: Drag behavior correctly handles scaling.

Key improvements:

  1. Auto-scroll (lines 241-249): Uses scrollIntoView to keep drag handles visible during resize, improving UX when handles are near viewport edges.
  2. Scale compensation (lines 257-258): Correctly divides clientWidth and clientHeight by scale when calling resize(), ensuring the stored dimensions represent the actual (unscaled) size rather than the visual size.
  3. Visual feedback (lines 235, 239): Updates data-value attributes on drag handles for real-time dimension display.

The scale-aware resize calculation properly handles the wrapper-based scaling approach.

Also applies to: 234-249, 256-258


294-301: The readOnly prop is valid and supported by ActionList.Action.

ActionList.Action extends ActionListButton, which extends Button, and the Button component explicitly accepts and defaults readOnly to false. The prop correctly propagates through the component hierarchy. No action needed on this point.

Regarding the aria-label and label props on SizeInput (lines 294-296, 315-317): while both are provided, this is acceptable. The label prop creates a visually-hidden <label> element, and aria-label is passed through to the input itself. Both can coexist on the same input; aria-label takes precedence for screen reader announcements. If reducing verbosity is desired, consider removing one of them, though both being present is not incorrect.

Likely an incorrect or invalid review comment.

@Sidnioulz Sidnioulz mentioned this pull request Jan 9, 2026
12 tasks
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @code/core/src/manager/components/preview/NumericInput.tsx:
- Around line 14-15: The code constructs a RegExp from user-controlled baseUnit
in NumericInput (new RegExp(`${baseUnit}$`)) which allows regex injection;
escape baseUnit before building the RegExp (or use a safe alternative like
RegExp.escape helper) so metacharacters are treated literally, then replace the
existing RegExp construction in NumericInput (the baseUnit variable and the
RegExp usage around the suffix strip logic) with the escaped value.
🧹 Nitpick comments (6)
code/core/src/manager/components/Shortcut.tsx (1)

30-35: Avoid potential React key collisions in keys.map(...).

Using key={key} (Line 33) can collide if the array contains duplicates; use an index-based key (or ${key}-${i}) to keep rendering stable.

Proposed change
 export const Shortcut = ({ keys }: { keys: string[] }) => (
   <Wrapper>
-    {keys.map((key) => (
-      <Key key={key}>{shortcutToHumanString([key])}</Key>
+    {keys.map((key, i) => (
+      <Key key={`${key}-${i}`}>{shortcutToHumanString([key])}</Key>
     ))}
   </Wrapper>
 );
code/core/src/manager/components/preview/NumericInput.tsx (1)

145-164: Prefer onKeyDown over a native addEventListener for ArrowUp/ArrowDown.

The current native listener (Line 161) is harder to compose with consumer handlers and is re-bound whenever inputValue changes. Consider moving this logic into a React onKeyDown and chaining with props.onKeyDown if provided.

code/core/src/manager/components/preview/NumericInput.stories.tsx (1)

16-105: Nice coverage for keyboard stepping; consider adding a “manual typing” story for parsing edge cases.

Current stories validate ArrowUp/Down well; adding one story that types values (decimals, negative, invalid suffix, empty) would protect parseValue() behavior from regressions.

code/core/src/manager/components/preview/Viewport.tsx (1)

227-250: Consider throttling scrollIntoView during drag.

Calling scrollIntoView on every mousemove (Lines 241-249) can cause avoidable reflow/jank; using requestAnimationFrame throttling (or only scrolling when the handle is near/outside the viewport) would be safer.

code/core/src/manager/components/preview/tools/zoom.tsx (2)

15-30: Avoid Context default no-op setter (fail fast if provider missing).

Right now, calling set() outside a ZoomProvider will silently do nothing, which is painful to diagnose—especially since ZoomConsumer/ZoomProvider are exported.

Proposed change
-const Context = createContext({ value: INITIAL_ZOOM_LEVEL, set: (v: number) => {} });
+const Context = createContext({
+  value: INITIAL_ZOOM_LEVEL,
+  set: (_v: number) => {
+    throw new Error('ZoomProvider is missing');
+  },
+});

63-155: Numeric input: allow decimals + clamp invalid/unsafe zoom values; make the leading icon non-interactive.

  • parseInt drops decimals (e.g. 12.5%12%), which undermines “custom zoom” expectations.
  • No guard against zoomTo(0) / negative values (can create broken scaling downstream).
  • The leading ActionList.Button is decorative but still a button element—ensure it’s not focusable/announced. (As per repo convention, prefer ariaLabel={false} when a Button’s children already provide the name; here it’s decorative so making it disabled + aria-hidden is safest.) Based on learnings, Button ariaLabel usage is intentionally standardized.
Proposed change
       popover={
         <>
           <ActionList>
             <ActionList.Item>
               <ZoomInput
                 aria-label="Zoom percentage"
                 ref={inputRef}
                 unit="%"
                 before={
-                  <ActionList.Button size="small" padding="small" readOnly aria-hidden>
+                  <ActionList.Button
+                    size="small"
+                    padding="small"
+                    disabled
+                    aria-hidden
+                    ariaLabel={false}
+                  >
                     <ZoomIcon />
                   </ActionList.Button>
                 }
                 after={
                   <ActionList.Button
                     size="small"
                     padding="small"
                     onClick={() => zoomTo(1)}
                     ariaLabel="Reset zoom"
                   >
                     <UndoIcon />
                   </ActionList.Button>
                 }
                 value={`${Math.round(value * 100)}%`}
                 minValue={1}
                 maxValue={1000}
                 setValue={(value: string) => {
-                  const zoomLevel = parseInt(value, 10) / 100;
-                  if (!Number.isNaN(zoomLevel)) {
-                    zoomTo(zoomLevel);
-                  }
+                  const percent = Number(String(value).replace('%', ''));
+                  const zoomLevel = percent / 100;
+                  if (Number.isFinite(zoomLevel) && zoomLevel > 0) zoomTo(zoomLevel);
                 }}
               />
             </ActionList.Item>
           </ActionList>
       onVisibleChange={(isVisible) => {
         if (isVisible) {
-          requestAnimationFrame(() => inputRef.current?.select());
+          requestAnimationFrame(() => {
+            inputRef.current?.focus();
+            inputRef.current?.select();
+          });
         }
       }}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80cbd5b and 56eaf1f.

📒 Files selected for processing (5)
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/manager/components/preview/NumericInput.stories.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
  • code/core/src/manager/components/preview/tools/zoom.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,ts,tsx,json,md,html,css,scss}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Format code using Prettier with yarn prettier --write <file>

Files:

  • code/core/src/manager/components/preview/NumericInput.stories.tsx
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run ESLint checks using yarn lint:js:cmd <file> or the full command cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives to fix linting errors before committing

Files:

  • code/core/src/manager/components/preview/NumericInput.stories.tsx
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode across all packages

Files:

  • code/core/src/manager/components/preview/NumericInput.stories.tsx
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Export functions from modules if they need to be tested
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/manager/components/preview/NumericInput.stories.tsx
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
code/{core,lib,addons,builders,frameworks,presets}/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use logger from storybook/internal/node-logger for server-side logging in Node.js code

Files:

  • code/core/src/manager/components/preview/NumericInput.stories.tsx
  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/components/Shortcut.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
🧠 Learnings (10)
📚 Learning: 2025-09-24T09:39:39.233Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32507
File: code/core/src/manager/globals/globals-module-info.ts:25-33
Timestamp: 2025-09-24T09:39:39.233Z
Learning: In Storybook, storybook/actions/decorator is a preview-only entrypoint and should not be included in manager globals configuration. The duplicatedKeys array in code/core/src/manager/globals/globals-module-info.ts is specifically for manager-side externalization, not preview entrypoints.

Applied to files:

  • code/core/src/manager/components/preview/NumericInput.stories.tsx
📚 Learning: 2025-11-24T17:49:59.279Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .cursor/rules/spy-mocking.mdc:0-0
Timestamp: 2025-11-24T17:49:59.279Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Document complex mock behaviors in Vitest tests

Applied to files:

  • code/core/src/manager/components/preview/NumericInput.stories.tsx
📚 Learning: 2025-12-22T22:03:40.123Z
Learnt from: CR
Repo: storybookjs/storybook PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-22T22:03:40.123Z
Learning: Applies to **/*.{test,spec}.{ts,tsx,js,jsx} : Cover all branches, conditions, edge cases, error paths, and different input variations in unit tests

Applied to files:

  • code/core/src/manager/components/preview/NumericInput.stories.tsx
📚 Learning: 2025-11-05T09:37:25.920Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/tooltip/WithTooltip.tsx:54-96
Timestamp: 2025-11-05T09:37:25.920Z
Learning: Repo: storybookjs/storybook — In code/core/src/components/components/tooltip/WithTooltip.tsx, the legacy WithTooltip implementation is intentionally reintroduced for backward compatibility and is deprecated; maintainers (per Sidnioulz) do not want maintenance or improvements on it. Prefer WithTooltipNew/Popover; avoid suggesting changes to WithTooltip.* going forward.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-11-05T09:38:47.712Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
  • code/core/src/manager/components/preview/NumericInput.tsx
  • code/core/src/manager/components/preview/Viewport.tsx
📚 Learning: 2025-11-25T11:09:33.798Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 33140
File: code/core/src/manager/components/sidebar/TagsFilter.tsx:247-259
Timestamp: 2025-11-25T11:09:33.798Z
Learning: In the storybookjs/storybook repository, PopoverProvider creates popovers with a dialog role, so using aria-haspopup="dialog" on buttons that trigger PopoverProvider is semantically correct.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-11-05T09:36:55.944Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Tabs/Tabs.stories.tsx:222-227
Timestamp: 2025-11-05T09:36:55.944Z
Learning: Repo: storybookjs/storybook PR: 32458 — In code/core/src/components/components/Button/Button.tsx (React/TypeScript), ButtonProps includes ariaLabel?: string | false and the component maps it to the DOM aria-label. Convention: ariaLabel is mandatory on all Button usages — provide a descriptive string for icon-only buttons; set ariaLabel=false when the button’s children already serve as the accessible name. Do not suggest using a raw aria-label prop on Button call sites.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-10-03T07:55:42.639Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/manager/components/preview/Toolbar.tsx:102-105
Timestamp: 2025-10-03T07:55:42.639Z
Learning: In code/core/src/manager/components/preview/Toolbar.tsx, we intentionally do not add aria-label/aria-labelledby to StyledToolbar (AbstractToolbar) because the enclosing section is already labeled via an sr-only heading and the toolbar is the only content. Revisit only if real user testing indicates a need.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-10-01T15:24:01.060Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32594
File: code/core/src/components/components/Popover/WithPopover.tsx:7-9
Timestamp: 2025-10-01T15:24:01.060Z
Learning: In the Storybook repository, "react-aria-components/patched-dist/*" (e.g., "react-aria-components/patched-dist/Dialog", "react-aria-components/patched-dist/Popover", "react-aria-components/patched-dist/Tooltip") are valid import paths created by a patch applied to the react-aria-components package. These imports should not be flagged as broken or invalid until a maintainer explicitly states they are no longer acceptable.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-09-18T20:51:52.320Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:32-37
Timestamp: 2025-09-18T20:51:52.320Z
Learning: NVDA screen reader announces the selected value in Select components even when aria-label is used, possibly due to special treatment of the role or aria-expanded attribute. Real-world screen reader testing may reveal that theoretical ARIA concerns don't always apply in practice.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
🧬 Code graph analysis (1)
code/core/src/manager/components/Shortcut.tsx (1)
code/core/src/manager-api/modules/shortcuts.ts (1)
  • keys (23-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: normal
  • GitHub Check: nx
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (2)
code/core/src/manager/components/preview/tools/zoom.tsx (2)

32-51: shouldScale=false still updates internal zoom state—confirm that’s intended.

set() always updates provider state even when shouldScale forces the exposed context value back to INITIAL_ZOOM_LEVEL. That’s fine if you want zoom “remembered” when scaling is re-enabled; otherwise consider ignoring set() while shouldScale is false.


214-215: Render wiring looks fine; just confirm ZoomProvider always wraps this tool.

If zoomTool can render without an ancestor ZoomProvider in some manager surfaces, the fail-fast Context change above will throw (which is desirable, but should be validated).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @code/core/src/manager/components/preview/tools/zoom.tsx:
- Line 114: The UI display uses Shortcut keys={['alt','+']} but the hotkey is
registered as ['alt','=']; update the Shortcut props to keys={['alt','=']} so
the visual label matches the actual binding (adjust the Shortcut component
instance and any other displayed instances), or alternatively change the hotkey
registration to ['alt','+'] if you intend the plus symbol to be the canonical
binding; ensure the Shortcut component and the hotkey registration use the same
array literal (refer to the Shortcut usage and the hotkey registration that
currently uses ['alt','=']).
- Line 173: The use of Array.prototype.findLast on ZOOM_LEVELS (assigning
lowerZoomLevel) is an ES2023 feature incompatible with the project's ES2020
target; replace it with an ES2020-compatible reverse-search: iterate ZOOM_LEVELS
from the last index down (or use a reversed shallow copy and .find) to locate
the first level < value and assign that to lowerZoomLevel, keeping the variable
name and semantics unchanged.
- Line 111: The usage of Array.prototype.at() (e.g., in the expression
disabled={value >= ZOOM_LEVELS.at(-1)!}) is an ES2022 feature while the project
targets ES2020; replace .at() usages in zoom.tsx (and the other occurrence
around the same block) with standard bracket indexing (use
ZOOM_LEVELS[ZOOM_LEVELS.length - 1] for .at(-1) and ZOOM_LEVELS[0] for .at(0)
where applicable) or ensure the build pipeline polyfills/transpiles .at()
appropriately so the code compiles under ES2020.
🧹 Nitpick comments (1)
code/core/src/manager/components/preview/tools/zoom.tsx (1)

138-142: Use consistent text wrapper for action items.

Unlike the other preset items (lines 129, 134), this 200% preset doesn't wrap its text in ActionList.Text. For consistency, consider:

♻️ Consistent text wrapping
 <ActionList.Item active={value === 2}>
   <ActionList.Action onClick={() => zoomTo(2)} ariaLabel="Zoom to 200%">
-    200%
+    <ActionList.Text>200%</ActionList.Text>
   </ActionList.Action>
 </ActionList.Item>
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56eaf1f and dfc576b.

📒 Files selected for processing (1)
  • code/core/src/manager/components/preview/tools/zoom.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,ts,tsx,json,md,html,css,scss}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Format code using Prettier with yarn prettier --write <file>

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Run ESLint checks using yarn lint:js:cmd <file> or the full command cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives to fix linting errors before committing

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Enable TypeScript strict mode across all packages

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Export functions from modules if they need to be tested
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
code/{core,lib,addons,builders,frameworks,presets}/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use logger from storybook/internal/node-logger for server-side logging in Node.js code

Files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
🧠 Learnings (8)
📚 Learning: 2025-11-05T09:37:25.920Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/tooltip/WithTooltip.tsx:54-96
Timestamp: 2025-11-05T09:37:25.920Z
Learning: Repo: storybookjs/storybook — In code/core/src/components/components/tooltip/WithTooltip.tsx, the legacy WithTooltip implementation is intentionally reintroduced for backward compatibility and is deprecated; maintainers (per Sidnioulz) do not want maintenance or improvements on it. Prefer WithTooltipNew/Popover; avoid suggesting changes to WithTooltip.* going forward.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-11-05T09:38:47.712Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-11-25T11:09:33.798Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 33140
File: code/core/src/manager/components/sidebar/TagsFilter.tsx:247-259
Timestamp: 2025-11-25T11:09:33.798Z
Learning: In the storybookjs/storybook repository, PopoverProvider creates popovers with a dialog role, so using aria-haspopup="dialog" on buttons that trigger PopoverProvider is semantically correct.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-11-05T09:36:55.944Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Tabs/Tabs.stories.tsx:222-227
Timestamp: 2025-11-05T09:36:55.944Z
Learning: Repo: storybookjs/storybook PR: 32458 — In code/core/src/components/components/Button/Button.tsx (React/TypeScript), ButtonProps includes ariaLabel?: string | false and the component maps it to the DOM aria-label. Convention: ariaLabel is mandatory on all Button usages — provide a descriptive string for icon-only buttons; set ariaLabel=false when the button’s children already serve as the accessible name. Do not suggest using a raw aria-label prop on Button call sites.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-10-03T07:55:42.639Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/manager/components/preview/Toolbar.tsx:102-105
Timestamp: 2025-10-03T07:55:42.639Z
Learning: In code/core/src/manager/components/preview/Toolbar.tsx, we intentionally do not add aria-label/aria-labelledby to StyledToolbar (AbstractToolbar) because the enclosing section is already labeled via an sr-only heading and the toolbar is the only content. Revisit only if real user testing indicates a need.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-10-01T15:24:01.060Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32594
File: code/core/src/components/components/Popover/WithPopover.tsx:7-9
Timestamp: 2025-10-01T15:24:01.060Z
Learning: In the Storybook repository, "react-aria-components/patched-dist/*" (e.g., "react-aria-components/patched-dist/Dialog", "react-aria-components/patched-dist/Popover", "react-aria-components/patched-dist/Tooltip") are valid import paths created by a patch applied to the react-aria-components package. These imports should not be flagged as broken or invalid until a maintainer explicitly states they are no longer acceptable.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-09-18T20:51:52.320Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:32-37
Timestamp: 2025-09-18T20:51:52.320Z
Learning: NVDA screen reader announces the selected value in Select components even when aria-label is used, possibly due to special treatment of the role or aria-expanded attribute. Real-world screen reader testing may reveal that theoretical ARIA concerns don't always apply in practice.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
📚 Learning: 2025-09-24T09:39:39.233Z
Learnt from: ndelangen
Repo: storybookjs/storybook PR: 32507
File: code/core/src/manager/globals/globals-module-info.ts:25-33
Timestamp: 2025-09-24T09:39:39.233Z
Learning: In Storybook, storybook/actions/decorator is a preview-only entrypoint and should not be included in manager globals configuration. The duplicatedKeys array in code/core/src/manager/globals/globals-module-info.ts is specifically for manager-side externalization, not preview entrypoints.

Applied to files:

  • code/core/src/manager/components/preview/tools/zoom.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
  • GitHub Check: nx
🔇 Additional comments (10)
code/core/src/manager/components/preview/tools/zoom.tsx (10)

1-16: LGTM: Clean imports and well-defined zoom levels.

The predefined zoom levels align with the PR's goal of providing predictability, and the initial zoom level of 100% is appropriate.


18-28: LGTM: Styled components are appropriately defined.

The fixed dimensions ensure consistent UI sizing.


22-22: LGTM: Context initialization follows standard React patterns.

The default no-op setter is appropriate for the initial context value.

Also applies to: 30-30


32-52: LGTM: ZoomProvider correctly manages zoom state.

The conditional zoom value based on shouldScale prop enables integration with the viewports feature as mentioned in the PR objectives.


54-69: LGTM: Popover configuration with smart auto-selection.

The use of requestAnimationFrame before selecting the input text is a good pattern to ensure the DOM is ready.


95-96: LGTM: Zoom range supports custom values as documented.

The 1-1000% range exceeds the previous 25-400% limitation, fulfilling the PR objective of supporting custom zoom percentages.


147-156: LGTM: Trigger button with clear accessibility label.

The active state visualization helps users identify when zoom is not at default.


210-216: LGTM: Tool registration correctly wired through context.

The render pattern properly connects the context consumer to the wrapper component.


186-205: No cleanup needed for shortcut registrations.

setAddonShortcut only updates state and stores shortcuts in configuration objects; it does not register listeners or create subscriptions. Calling it multiple times with the same actionName simply overwrites the previous value, so no cleanup is required.

Likely an incorrect or invalid review comment.


79-79: The readOnly prop is a valid and fully supported prop on the Button component in this codebase. No changes needed — the implementation is correct.

Likely an incorrect or invalid review comment.

@ghengeveld ghengeveld merged commit 9511a9c into next Jan 9, 2026
68 checks passed
@ghengeveld ghengeveld deleted the zoom-tool branch January 9, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants