Skip to content

Conversation

@ghengeveld
Copy link
Member

@ghengeveld ghengeveld commented Jan 21, 2026

What I did

Fixed custom viewports with complex CSS values such as calc(100% - 100px. These values now properly apply to the viewport, even when zooming, but cannot be updated since they aren't URL serializable.

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

Caution

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

    • Added support for CSS calc()-based viewport dimensions enabling more flexible sizing options.
    • Introduced dimension locking allowing independent control of viewport width and height editability.
  • Improvements

    • Enhanced visual feedback for disabled numeric input fields with improved styling.
    • Simplified viewport dimension calculation logic for more reliable behavior.
  • Testing

    • Added new viewport configuration story for testing calculated dimensions.

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

@nx-cloud
Copy link

nx-cloud bot commented Jan 21, 2026

View your CI Pipeline Execution ↗ for commit 67cb79e

Command Status Duration Result
nx run-many -t compile,check,knip,test,pretty-d... ❌ Failed 9m 3s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-21 15:27:43 UTC

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

This PR modifies viewport dimension handling to support CSS calc-based sizing and adds a locking mechanism that prevents resizing along specific axes. Changes include styling updates for disabled inputs, refactored dimension calculation logic, and simplified resize event handling.

Changes

Cohort / File(s) Summary
Viewport styling updates
code/core/src/manager/components/preview/NumericInput.tsx
Added disabled state styling: transparent background for disabled inputs and cursor-not-allowed for wrapper with disabled input
Viewport dimension logic
code/core/src/manager/components/preview/Viewport.tsx
Replaced frame calculation with CSS calc-based sizing; removed parseNumber helper and inlined numeric extraction; added dimensions.locked state to conditionally disable width/height inputs and render drag handles based on lock status
Viewport resize handling
code/core/src/viewport/useViewport.ts
Simplified resize handler by removing URL_VALUE_PATTERN parsing and pixel validation constraints; now unconditionally updates with computed values
Viewport test story
code/core/src/manager/components/preview/Viewport.stories.tsx
Added new viewport entry calc with CSS calc-based dimensions (calc(100% - 50px)) and exported Calculated story to test computed viewport sizing

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Core: Viewport UX fixes #33557: Directly modifies the same Viewport implementation to change how dimensions/frames are computed and how drag handles behave
✨ Finishing touches
  • 📝 Generate docstrings

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

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 275-292: The per-axis lock in the dimensions useMemo allows
resizing when one axis is complex; compute an isLocked flag (e.g., const
isLocked = !nx || !ny) inside the same useMemo and set locked: { width:
isLocked, height: isLocked } so both axes are locked if either dimension is
non-serializable; update any downstream checks that currently read locked.width
or locked.height to use the single isLocked value (or the updated locked object)
so resizing/resolution logic treats the pair as locked when either axis is
complex.
🧹 Nitpick comments (1)
code/core/src/manager/components/preview/NumericInput.tsx (1)

49-63: Consider a non-:has() fallback for disabled styling.

:has() support is still uneven in some environments. If Storybook manager must run in older browsers, the disabled styling won’t apply. A simple data-attribute fallback keeps visuals consistent.

🔧 Possible fallback
 const Wrapper = styled.div<{ after?: ReactNode; before?: ReactNode }>(
   ({ after, before, theme }) => ({
     ...
-    '&:has(input:disabled)': {
+    '&:has(input:disabled), &[data-disabled="true"]': {
       background: theme.base === 'light' ? theme.color.lighter : theme.input.background,
       cursor: 'not-allowed',
     },
     ...
   })
 );
-    <Wrapper after={after} before={before} className={className} style={style}>
+    <Wrapper
+      after={after}
+      before={before}
+      className={className}
+      style={style}
+      data-disabled={props.disabled ? 'true' : undefined}
+    >

Comment on lines 275 to 292
const dimensions = useMemo(() => {
const { number: nx, unit: ux = 'px' } = parseNumber(width) ?? { number: 0, unit: 'px' };
const { number: ny, unit: uy = 'px' } = parseNumber(height) ?? { number: 0, unit: 'px' };
const frameWidth = Math.max(VIEWPORT_MIN_WIDTH, nx * scale);
const frameHeight = Math.max(VIEWPORT_MIN_HEIGHT, ny * scale);
const [, nx = '', ux = 'px'] = width.match(/^(\d+(?:\.\d+)?)(\%|[a-z]{1,4})?$/) || [];
const [, ny = '', uy = 'px'] = height.match(/^(\d+(?:\.\d+)?)(\%|[a-z]{1,4})?$/) || [];
return {
frame: {
width: `${frameWidth}${ux}`,
height: `${frameHeight}${uy}`,
width: `calc(${width} * ${scale})`,
height: `calc(${height} * ${scale})`,
},
display: {
width: `${nx}${ux === 'px' ? '' : ux}`,
height: `${ny}${uy === 'px' ? '' : uy}`,
width: `${nx || width}${ux === 'px' ? '' : ux}`,
height: `${ny || height}${uy === 'px' ? '' : uy}`,
},
locked: {
width: !nx,
height: !ny,
},
};
}, [width, height, scale]);
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

Disable all resizing when either dimension is non-serializable.

With the current per-axis lock, a mixed case (e.g., width: calc(...), height: 400px) still allows resizing height. That calls resize(width, height) with a complex width, producing a non-URL-serializable value that later falls back to the first viewport option. This breaks the viewport selection and conflicts with the “resizing disabled for complex values” intent.

✅ Suggested fix: lock both axes if either is complex
   const dimensions = useMemo(() => {
     const [, nx = '', ux = 'px'] = width.match(/^(\d+(?:\.\d+)?)(\%|[a-z]{1,4})?$/) || [];
     const [, ny = '', uy = 'px'] = height.match(/^(\d+(?:\.\d+)?)(\%|[a-z]{1,4})?$/) || [];
+    const isLocked = !nx || !ny;
     return {
       frame: {
         width: `calc(${width} * ${scale})`,
         height: `calc(${height} * ${scale})`,
       },
       display: {
         width: `${nx || width}${ux === 'px' ? '' : ux}`,
         height: `${ny || height}${uy === 'px' ? '' : uy}`,
       },
       locked: {
-        width: !nx,
-        height: !ny,
+        width: isLocked,
+        height: isLocked,
       },
     };
   }, [width, height, scale]);
-              disabled={dimensions.locked.width}
+              disabled={dimensions.locked.width}
...
-              disabled={dimensions.locked.height}
+              disabled={dimensions.locked.height}
...
-        {!dimensions.locked.width && (
+        {!dimensions.locked.width && (
           <DragHandle ... />
         )}
-        {!dimensions.locked.height && (
+        {!dimensions.locked.height && (
           <DragHandle ... />
         )}
-        {!dimensions.locked.width && !dimensions.locked.height && (
+        {!dimensions.locked.width && !dimensions.locked.height && (
           <DragHandle ... />
         )}

(With isLocked, all three checks become false when either axis is complex.)

Also applies to: 311-392

🤖 Prompt for AI Agents
In `@code/core/src/manager/components/preview/Viewport.tsx` around lines 275 -
292, The per-axis lock in the dimensions useMemo allows resizing when one axis
is complex; compute an isLocked flag (e.g., const isLocked = !nx || !ny) inside
the same useMemo and set locked: { width: isLocked, height: isLocked } so both
axes are locked if either dimension is non-serializable; update any downstream
checks that currently read locked.width or locked.height to use the single
isLocked value (or the updated locked object) so resizing/resolution logic
treats the pair as locked when either axis is complex.

@storybook-app-bot
Copy link

Package Benchmarks

Commit: 67cb79e, ran on 21 January 2026 at 15:27:40 UTC

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

@storybook/builder-webpack5

Before After Difference
Dependency count 192 192 0
Self size 75 KB 75 KB 🎉 -51 B 🎉
Dependency size 32.24 MB 32.25 MB 🚨 +11 KB 🚨
Bundle Size Analyzer Link Link

storybook

Before After Difference
Dependency count 49 49 0
Self size 20.30 MB 20.32 MB 🚨 +21 KB 🚨
Dependency size 16.52 MB 16.52 MB 🎉 -2 B 🎉
Bundle Size Analyzer Link Link

@storybook/ember

Before After Difference
Dependency count 196 196 0
Self size 15 KB 15 KB 🚨 +15 B 🚨
Dependency size 28.96 MB 28.97 MB 🚨 +11 KB 🚨
Bundle Size Analyzer Link Link

@storybook/nextjs

Before After Difference
Dependency count 538 538 0
Self size 646 KB 646 KB 🎉 -5 B 🎉
Dependency size 59.22 MB 59.73 MB 🚨 +505 KB 🚨
Bundle Size Analyzer Link Link

@storybook/nextjs-vite

Before After Difference
Dependency count 127 127 0
Self size 1.12 MB 1.12 MB 🚨 +19 B 🚨
Dependency size 21.82 MB 22.32 MB 🚨 +496 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-native-web-vite

Before After Difference
Dependency count 159 159 0
Self size 30 KB 30 KB 🚨 +13 B 🚨
Dependency size 23.00 MB 23.61 MB 🚨 +610 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-vite

Before After Difference
Dependency count 117 117 0
Self size 35 KB 35 KB 🎉 -4 B 🎉
Dependency size 19.62 MB 20.11 MB 🚨 +496 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-webpack5

Before After Difference
Dependency count 278 278 0
Self size 24 KB 24 KB 🚨 +7 B 🚨
Dependency size 44.13 MB 44.64 MB 🚨 +505 KB 🚨
Bundle Size Analyzer Link Link

@storybook/server-webpack5

Before After Difference
Dependency count 204 204 0
Self size 16 KB 16 KB 🎉 -5 B 🎉
Dependency size 33.49 MB 33.50 MB 🚨 +11 KB 🚨
Bundle Size Analyzer Link Link

@storybook/vue3-vite

Before After Difference
Dependency count 114 114 0
Self size 35 KB 35 KB 🎉 -4 B 🎉
Dependency size 43.98 MB 43.96 MB 🎉 -18 KB 🎉
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 183 183 0
Self size 775 KB 775 KB 🚨 +224 B 🚨
Dependency size 67.38 MB 67.47 MB 🚨 +92 KB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 176 176 0
Self size 30 KB 30 KB 🎉 -38 B 🎉
Dependency size 65.95 MB 66.05 MB 🚨 +92 KB 🚨
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 50 50 0
Self size 1000 KB 999 KB 🎉 -47 B 🎉
Dependency size 36.82 MB 36.84 MB 🚨 +21 KB 🚨
Bundle Size Analyzer node node

@storybook/preset-react-webpack

Before After Difference
Dependency count 170 170 0
Self size 18 KB 18 KB 🚨 +21 B 🚨
Dependency size 31.26 MB 31.28 MB 🚨 +11 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react

Before After Difference
Dependency count 57 57 0
Self size 732 KB 1.23 MB 🚨 +494 KB 🚨
Dependency size 12.94 MB 12.94 MB 🚨 +2 KB 🚨
Bundle Size Analyzer Link Link

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.

2 participants