diff --git a/.storybook/main.js b/.storybook/main.js
index a8c71dd3e6..4a60775617 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -11,6 +11,7 @@ export default {
'@storybook/addon-docs',
'@storybook/addon-vitest',
'storybook-addon-tag-badges',
+ 'storybook-font-inspector',
],
docs: {
diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
index 05da1e9dfb..dfe615d866 100644
--- a/.storybook/preview-head.html
+++ b/.storybook/preview-head.html
@@ -1,3 +1,23 @@
+
+
+
\ No newline at end of file
diff --git a/.storybook/preview.js b/.storybook/preview.js
index 25339fa079..ba05fa65a8 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -1,15 +1,19 @@
import React from 'react';
-import '@fontsource-variable/source-sans-3';
import '../src/assets/styles/_shared.scss';
import themeCFPB from './themeCFPB';
const responsivePreviewQueryParameter = 'responsivePreview';
+// Match CFPB design-system breakpoints (16px root): large layout from 63.8125em (~1021px).
+// Hero `.m-hero__wrapper` uses min-height + em padding there; 1230px aligns with typical
+// max-width + gutters. Storybook also accepts ad-hoc sizes via globals.viewport.value like
+// `1230-900` (width-height, px by default) without adding an entry here.
const viewportOptions = {
desktop: {
name: 'Desktop (901px and above)',
styles: {
- width: '1280px',
+ // Match design width; iframe uses content-box so border does not shrink the inner viewport.
+ width: '1230px',
height: '900px',
},
type: 'desktop',
@@ -34,6 +38,10 @@ const viewportOptions = {
const responsivePreviewOptions = Object.entries(viewportOptions);
+/** Extra height on nested All-viewports iframes so :focus-visible rings are not clipped (outline
+ * does not affect layout / scrollHeight). Covers checkbox, large target, fieldset, select, etc. */
+const RESPONSIVE_PREVIEW_FOCUS_VERTICAL_BUFFER_PX = 40;
+
const shouldRenderSinglePreview = (context) => {
const searchParameters = new URLSearchParams(globalThis.location.search);
@@ -63,26 +71,26 @@ const getFrameHeight = (frame) => {
const { body, documentElement } = frameDocument;
const storyRoot = frameDocument.getElementById('storybook-root');
- if (storyRoot) {
- const bodyStyles = frame.contentWindow?.getComputedStyle(body);
- const verticalPadding =
- parseFloat(bodyStyles?.paddingTop ?? '0') +
- parseFloat(bodyStyles?.paddingBottom ?? '0');
- const rootTop = storyRoot.getBoundingClientRect().top;
- const contentBottom = Array.from(storyRoot.querySelectorAll('*')).reduce(
- (bottom, element) =>
- Math.max(bottom, element.getBoundingClientRect().bottom),
- storyRoot.getBoundingClientRect().bottom,
+ if (storyRoot && body) {
+ const win = frame.contentWindow;
+ const bodyStyle = win?.getComputedStyle(body);
+ const bodyVerticalPadding =
+ parseFloat(bodyStyle?.paddingTop ?? '0') +
+ parseFloat(bodyStyle?.paddingBottom ?? '0');
+
+ // Prefer #storybook-root box for content height. `body.scrollHeight` alone often tracks the
+ // iframe’s current height (min-height / 100% chains). Add body vertical padding when the body
+ // has inset (single canvas). Do not add #storybook-root padding again — offsetHeight /
+ // scrollHeight already include it when `box-sizing: border-box` (nested All viewports).
+ const fromRoot = Math.max(
+ storyRoot.scrollHeight,
+ storyRoot.offsetHeight,
+ storyRoot.getBoundingClientRect().height,
);
- return Math.ceil(
- Math.max(
- storyRoot.getBoundingClientRect().height,
- storyRoot.scrollHeight,
- storyRoot.offsetHeight,
- contentBottom - rootTop,
- ) + verticalPadding,
- );
+ if (fromRoot > 0) {
+ return Math.ceil(fromRoot + bodyVerticalPadding);
+ }
}
return Math.max(
@@ -94,12 +102,13 @@ const getFrameHeight = (frame) => {
};
const ResponsivePreviewFrame = ({ storyId, viewport }) => {
- const [height, setHeight] = React.useState(240);
+ const [height, setHeight] = React.useState(64);
const updateHeight = React.useCallback((frame) => {
const measuredHeight = getFrameHeight(frame);
-
- setHeight(Math.max(measuredHeight + 16, 160));
+ const padded = measuredHeight + RESPONSIVE_PREVIEW_FOCUS_VERTICAL_BUFFER_PX;
+ // Floor avoids 0 during load; buffer leaves room for focus outlines outside the layout box.
+ setHeight(Math.max(Math.ceil(padded), 1));
}, []);
return React.createElement('iframe', {
@@ -130,8 +139,12 @@ const ResponsivePreviewFrame = ({ storyId, viewport }) => {
title: `${viewport.name} preview`,
style: {
background: 'white',
- border: '1px solid #d0d0ce',
- boxSizing: 'border-box',
+ // set border around the iframe
+ border: 'none',
+ // border-box would make width include the border, so a 900px frame only has ~898px for
+ // the document — content-box keeps viewport.styles.width as the iframe layout width.
+ boxSizing: 'content-box',
+ display: 'block',
height,
width: viewport.styles.width,
},
@@ -149,9 +162,9 @@ const renderResponsivePreviews = (Story, context) => {
style: {
boxSizing: 'border-box',
display: 'grid',
- gap: '24px',
+ gap: '45px',
overflowX: 'auto',
- padding: '24px',
+ padding: '0px',
},
},
responsivePreviewOptions.map(([key, viewport]) =>
@@ -161,19 +174,16 @@ const renderResponsivePreviews = (Story, context) => {
key,
style: {
display: 'grid',
- gap: '8px',
+ // gap: '15px',
justifyItems: 'start',
},
},
React.createElement(
- 'div',
+ 'p',
{
style: {
- color: '#5a5d61',
- fontFamily: 'Source Sans 3 Variable, sans-serif',
- fontSize: '14px',
- fontWeight: 600,
- lineHeight: 1.25,
+ color: '#43484e',
+ fontWeight: 500,
},
},
`${viewport.name}`,
@@ -187,6 +197,45 @@ const renderResponsivePreviews = (Story, context) => {
);
};
+/** Storybook body classes applied by `parameters.layout` (see prepareForStory / WebView). */
+const STORYBOOK_LAYOUT_BODY_CLASSES = [
+ 'sb-main-padded',
+ 'sb-main-centered',
+ 'sb-main-fullscreen',
+];
+
+/**
+ * Only force `sb-main-fullscreen` when a story opts in with `parameters.layout: 'fullscreen'`.
+ * Global `layout: 'fullscreen'` in preview was removed because it merges into docs `