From b9ea6038507d4620b64d9aef99616ae46316855b Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 26 May 2026 22:36:05 +0200 Subject: [PATCH 01/14] Add a baseline-vs-latest comparison experience to the review details screen with 1up/2up modes, view toggles, and readiness gating so latest stays visible while baseline loads. Also proxy baseline previews through a local __review-baseline dev-server route and keep the two iframes aligned by syncing URL and scroll position. --- code/addons/review/package.json | 1 + code/addons/review/src/preset.ts | 18 +- .../src/screens/DetailsScreen.stories.tsx | 20 +- .../review/src/screens/DetailsScreen.tsx | 415 +++++++++++++++--- code/core/src/manager/settings/GuidePage.tsx | 2 +- yarn.lock | 65 ++- 6 files changed, 434 insertions(+), 87 deletions(-) diff --git a/code/addons/review/package.json b/code/addons/review/package.json index 3252237856d7..3436641e426f 100644 --- a/code/addons/review/package.json +++ b/code/addons/review/package.json @@ -42,6 +42,7 @@ "!src/**/*" ], "devDependencies": { + "http-proxy-middleware": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.9.3" diff --git a/code/addons/review/src/preset.ts b/code/addons/review/src/preset.ts index 81bed879c5f4..82e6b6bd75c3 100644 --- a/code/addons/review/src/preset.ts +++ b/code/addons/review/src/preset.ts @@ -1,5 +1,6 @@ +import { createProxyMiddleware } from 'http-proxy-middleware'; import type { Channel } from 'storybook/internal/channels'; -import type { Options } from 'storybook/internal/types'; +import type { Middleware, Options, ServerApp } from 'storybook/internal/types'; import { EVENTS } from './constants.ts'; import type { ReviewState } from './review-state.ts'; @@ -70,3 +71,18 @@ export const experimental_serverChannel = async ( return channel; }; + +const BASELINE_PROXY_PATH = '/__review-baseline'; +const BASELINE_TARGET_ORIGIN = 'https://next--635781f3500dd2c49e189caf.chromatic.com'; + +export const experimental_devServer = (app: ServerApp) => { + const proxyRequest = createProxyMiddleware({ + target: BASELINE_TARGET_ORIGIN, + changeOrigin: true, + pathRewrite: (path) => + path.startsWith(BASELINE_PROXY_PATH) ? path.slice(BASELINE_PROXY_PATH.length) || '/' : path, + }) as unknown as Middleware; + + app.use(BASELINE_PROXY_PATH, proxyRequest); + return app; +}; diff --git a/code/addons/review/src/screens/DetailsScreen.stories.tsx b/code/addons/review/src/screens/DetailsScreen.stories.tsx index 30254b5555e5..5a91609039b1 100644 --- a/code/addons/review/src/screens/DetailsScreen.stories.tsx +++ b/code/addons/review/src/screens/DetailsScreen.stories.tsx @@ -11,10 +11,10 @@ const meta = preview.meta({ component: DetailsScreen, parameters: { layout: 'fullscreen' }, args: { - title: 'Toolbar', - componentTitle: 'Manager/Components/Toolbar', - storyName: 'Basic', - storyId: 'components-toolbar--basic', + title: 'Guide Page', + componentTitle: 'Manager/Settings/GuidePage', + storyName: 'Default', + storyId: 'manager-settings-guidepage--default', storyIndex: 1, totalStories: 3, backHref: buildReviewChangesSummaryHref(), @@ -28,7 +28,6 @@ const meta = preview.meta({ collectionIndex: 0, storyId: 'components-toolbar--dense', }), - branchName: 'update/button-weight-and-padding', }, }); @@ -36,17 +35,20 @@ export const Default = meta.story({ play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(await canvas.findByRole('button', { name: '2/3' })).toBeInTheDocument(); - await expect(await canvas.findByText('Toolbar')).toBeInTheDocument(); - await expect(await canvas.findByText('Basic')).toBeInTheDocument(); + await expect(await canvas.findByText('GuidePage')).toBeInTheDocument(); + await expect(await canvas.findByText('Default')).toBeInTheDocument(); await expect( - await canvas.findByText('Latest on update/button-weight-and-padding') + await canvas.findByTitle('Baseline manager-settings-guidepage--default') + ).toBeInTheDocument(); + await expect( + await canvas.findByTitle('Latest manager-settings-guidepage--default') ).toBeInTheDocument(); }, }); export const WrapAroundNavigation = meta.story({ args: { - storyId: 'components-toolbar--basic', + storyId: 'manager-settings-guidepage--default', storyIndex: 0, totalStories: 3, previousHref: buildReviewChangesDetailHref({ diff --git a/code/addons/review/src/screens/DetailsScreen.tsx b/code/addons/review/src/screens/DetailsScreen.tsx index d21fab97b4df..6382fc7a2133 100644 --- a/code/addons/review/src/screens/DetailsScreen.tsx +++ b/code/addons/review/src/screens/DetailsScreen.tsx @@ -1,9 +1,16 @@ -import React, { type FC } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Button } from 'storybook/internal/components'; +import { STORY_RENDERED } from 'storybook/internal/core-events'; import { styled } from 'storybook/theming'; -import { ChevronSmallLeftIcon, ChevronSmallRightIcon } from '@storybook/icons'; +import { + CategoryIcon, + ChevronSmallLeftIcon, + ChevronSmallRightIcon, + SideBySideIcon, + TransferIcon, +} from '@storybook/icons'; const Page = styled.div(({ theme }) => ({ display: 'flex', @@ -57,12 +64,49 @@ const DetailTitleRegular = styled.span({ fontWeight: 400, }); -const PreviewFrameWrap = styled.div({ +type PreviewMode = '1up' | '2up'; +type VisibleSide = 'baseline' | 'latest'; + +const PreviewFrameWrap = styled.div<{ $singleUp: boolean }>(({ $singleUp }) => ({ flex: 1, minHeight: 0, width: '100%', display: 'flex', -}); + position: 'relative', + ...($singleUp ? { overflow: 'hidden' } : {}), +})); + +const PreviewPane = styled.div<{ $singleUp: boolean; $active: boolean }>( + ({ $singleUp, $active }) => ({ + minWidth: 0, + minHeight: 0, + display: 'flex', + ...($singleUp + ? { + position: 'absolute', + inset: 0, + width: '100%', + height: '100%', + zIndex: $active ? 1 : -1, + pointerEvents: $active ? 'auto' : 'none', + visibility: $active ? 'visible' : 'hidden', + } + : { + flex: 1, + position: 'relative', + }), + }) +); + +const PaneDivider = styled.div(({ theme }) => ({ + width: 1, + flexShrink: 0, + background: theme.color.border, +})); + +const PreviewDivider = styled(PaneDivider)<{ $singleUp: boolean }>(({ $singleUp }) => ({ + display: $singleUp ? 'none' : 'block', +})); const PreviewFrame = styled.iframe({ flex: 1, @@ -75,25 +119,50 @@ const PreviewFrame = styled.iframe({ const BottomToolbar = styled.div(({ theme }) => ({ display: 'flex', alignItems: 'center', - justifyContent: 'space-between', - gap: 6, minHeight: 40, - padding: '0 10px 0 16px', borderTop: `1px solid ${theme.appBorderColor}`, })); -const BranchText = styled.div({ +const BottomHalf = styled.div({ + flex: 1, + minWidth: 0, + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + padding: '0 16px', +}); + +const BottomDivider = styled(PaneDivider)<{ $singleUp: boolean }>(({ $singleUp }) => ({ + display: $singleUp ? 'none' : 'block', +})); + +const RightBottomHalf = styled(BottomHalf)<{ $singleUp: boolean }>(({ $singleUp }) => ({ + justifyContent: $singleUp ? 'flex-end' : 'space-between', + gap: 8, +})); + +const BottomControls = styled.div({ + display: 'flex', + alignItems: 'center', + gap: 4, +}); + +const BottomLabel = styled.div({ fontSize: 13, lineHeight: '20px', fontWeight: 600, }); -const BottomRightPlaceholder = styled.div({ - width: 24, - height: 24, -}); - +const BASELINE_PROXY_PATH = '/__review-baseline'; const storyPreviewUrl = (id: string) => `iframe.html?id=${encodeURIComponent(id)}&viewMode=story`; +const toBaselinePreviewUrl = (latestUrlString: string) => { + const latestUrl = new URL(latestUrlString, window.location.href); + return new URL( + `${BASELINE_PROXY_PATH}${latestUrl.pathname}${latestUrl.search}${latestUrl.hash}`, + window.location.origin + ).toString(); +}; export interface DetailsScreenProps { /** Fallback title shown when story metadata is unavailable. */ @@ -137,7 +206,7 @@ const renderDetailTitle = ({ // The preview