Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
'@storybook/addon-docs',
'@storybook/addon-vitest',
'storybook-addon-tag-badges',
'storybook-font-inspector',
],

docs: {
Expand Down
20 changes: 20 additions & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
<script>
window.global = window;
</script>

<!-- Storybook default: `padding: 1rem` on `.sb-show-main.sb-main-padded` (base-preview-head.html).
- Main canvas: CFPB 15px ($space-sm).
- Nested "All viewports" (`?responsivePreview=off`): body padding 0; `#storybook-root` uses
vertical inset for focus plus small horizontal inset so outlines are not clipped at edges. -->
<script>
(function applyCfpbStorybookCanvasPadding() {
var style = document.createElement('style');
if (new URLSearchParams(window.location.search).get('responsivePreview') === 'off') {
style.textContent =
'.sb-show-main.sb-main-padded { padding: 0 !important; }' +
'body.sb-show-main.sb-main-padded #storybook-root, body.sb-show-main.sb-main-centered #storybook-root { padding: 10px 3px !important; box-sizing: border-box !important; }';
} else {
style.textContent =
'.sb-show-main.sb-main-padded { padding: 30px; }' +
'.sb-show-main.sb-main-centered #storybook-root { padding: 15px; }';
}
document.head.appendChild(style);
})();
</script>
132 changes: 94 additions & 38 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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);

Expand Down Expand Up @@ -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(
Expand All @@ -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', {
Expand Down Expand Up @@ -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,
},
Expand All @@ -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]) =>
Expand All @@ -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}`,
Expand All @@ -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 `<Canvas>`.
*
* For the default (undefined / `padded`), Storybook already applies `sb-main-padded` — the same
* ~1rem inset as Overview / autodocs previews. Do not override that here.
*
* @type {(Story: any, context: any) => import('react').ReactElement}
*/
const withExplicitFullscreenStoryCanvas = (Story, context) => {
React.useLayoutEffect(() => {
if (context.viewMode !== 'story') {
return undefined;
}

if (context.parameters?.layout !== 'fullscreen') {
return undefined;
}

const { body } = document;

for (const className of STORYBOOK_LAYOUT_BODY_CLASSES) {
body.classList.remove(className);
}

body.classList.add('sb-main-fullscreen');
return undefined;
}, [context.viewMode, context.id, context.parameters?.layout]);

return React.createElement(Story);
};

export const globalTypes = {
responsivePreview: {
name: 'Responsive preview',
Expand All @@ -203,19 +252,26 @@ export const globalTypes = {

export const initialGlobals = {
responsivePreview: 'single',
viewport: {
value: 'responsive',
},
// Omit `globals.viewport` so the toolbar defaults to "Reset viewport" (full canvas).
// Setting `value: 'desktop'` (or any named key) forces that preset for every story.
};

export const decorators = [renderResponsivePreviews];
export const decorators = [
renderResponsivePreviews,
withExplicitFullscreenStoryCanvas,
];

export const preview = {
globalTypes,

initialGlobals,

parameters: {
// Default canvas padding matches Overview `<Canvas>` (`sb-main-padded`, 1rem). Stories that
// need edge-to-edge can set `parameters.layout: 'fullscreen'` (see
// `withExplicitFullscreenStoryCanvas`).
// https://storybook.js.org/docs/configure/story-layout

viewport: {
options: viewportOptions,
},
Expand Down
2 changes: 1 addition & 1 deletion .storybook/themeCFPB.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default create({
brandUrl: 'https://cfpb.github.io/design-system-react/',
brandTarget: '_blank',

fontBase: '"Source Sans 3 Variable", Arial ,sans-serif',
fontBase: '"Source Sans 3 Variable", Arial, sans-serif',

// App
appBorderColor: colors.gray,
Expand Down
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@cfpb/cfpb-design-system": "5.3.3",
"@chromatic-com/storybook": "^5.1.2",
"@eslint/js": "^10.0.1",
"@fontsource-variable/source-sans-3": "5.2.9",
"@nabla/vite-plugin-eslint": "^3.0.1",
"@storybook/addon-a11y": "^10.3.6",
"@storybook/addon-docs": "^10.3.6",
Expand Down Expand Up @@ -113,6 +114,7 @@
"start-server-and-test": "3.0.2",
"storybook": "^10.3.6",
"storybook-addon-tag-badges": "^3.1.0",
"storybook-font-inspector": "^1.1.7",
"stylelint": "^17.11.0",
"stylelint-config-standard-scss": "^17.0.0",
"typescript": "^6.0.3",
Expand Down
67 changes: 46 additions & 21 deletions src/assets/styles/_shared.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@use './variables';
@use 'sass:math';
@use 'sass:string';
@use '@cfpb/cfpb-design-system/src/index' as *;
@use '@cfpb/cfpb-design-system/src/elements/abstracts' as *;
@use '@cfpb/cfpb-design-system/src/elements/base' as base;
Expand All @@ -11,20 +12,45 @@
*/

/**
/* Storybook components are not rendered within a CFPB layout so
/* the DS' max-width restriction for text content is not applied.
/*
/* https://github.com/cfpb/design-system/blob/main/packages/cfpb-layout/src/cfpb-layout.scss#L210-L223
/*
/* A similar adjustment was made for the DS docs pages
/* https://github.com/cfpb/design-system/pull/1220
**/
:root,
* Source Sans 3 is loaded via @fontsource in app / Storybook preview entrypoints.
*
* DS `custom-props` sets `--font-stack-branded: initial`, so `var(--font-stack)` falls back to
* `system-ui`. Other DS chunks can repeat that `:root` block when code-split; in dev the last
* chunk can win and wipe a non-`!important` override. Pin both tokens with `!important` so inputs,
* buttons, and body always match.
*
* `html` / `body` / Storybook roots also get an explicit stack so we are not only relying on
* custom properties for the main canvas.
*/
// Literal stack (quoted multi-word family) — matches app / design-system expectations.
$_ds-font-stack: string.unquote('"Source Sans 3 Variable", Arial, sans-serif');

:root {
--font-stack-branded: #{$_ds-font-stack} !important;
--font-stack: #{$_ds-font-stack} !important;
}

// `!important` beats DS normalize `html { font-family: sans-serif }` if chunk
// order ever places that rule after this file (e.g. code-splitting in dev).
html,
body,
#storybook-root,
#storybook-docs {
--font-stack: 'Source Sans 3 Variable', 'Source Sans Pro', 'Arial', sans-serif;
font-family: var(--font-stack);
font-family: #{$_ds-font-stack} !important;
}

/**
* Long-form / layout simulation for the Docs tab only.
*
* Do not target `#storybook-root` here: the story canvas should match production
* (DS + component CSS only). Rules like paragraph max-width were affecting heroes
* and other components inside the iframe.
*
* DS max-width context:
* https://github.com/cfpb/design-system/blob/main/packages/cfpb-layout/src/cfpb-layout.scss#L210-L223
* https://github.com/cfpb/design-system/pull/1220
*/
#storybook-docs {
dd,
dt,
label,
Expand All @@ -41,23 +67,22 @@
font-size: 16px;
}

/*
Make code block plain-text readable (workaround since we can't change code highlighting theme)
/*
Make code block plain-text readable (workaround since we can't change code highlighting theme)
https://github.com/storybookjs/storybook/issues/9641
*/
pre {
background-color: #2b2b2b;
color: white;
line-height: 22px !important;
}
}

/*
Apply a global line-height that doesn't interfere with component Stories
that have their own line-height settings.
*/
pre,
#storybook-docs :where(p:not(.sb-story *)) {
line-height: 22px !important;
/*
Readable docs prose line-height (avoid touching `.sb-story` embeds).
*/
:where(p:not(.sb-story *)) {
line-height: 22px !important;
}
}

// Override DS wrapper match-content width to 1170px content with 30px gutters.
Expand Down
Loading
Loading