Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
54d8c5f
feat(storybook): add @fluentui/react-storybook-addon-playground with …
dmytrokirpa Sep 16, 2026
fa6a09c
Merge upstream/master into experimental/storybook-playground
dmytrokirpa Sep 16, 2026
66e9370
fix(storybook): address playground review feedback
dmytrokirpa Sep 16, 2026
bd38b94
change
dmytrokirpa Sep 16, 2026
2652778
update yarn.lock
dmytrokirpa Sep 16, 2026
f8fc7b2
fix(storybook): align playground postcss version
dmytrokirpa Sep 16, 2026
ee9303b
address review comments
dmytrokirpa Sep 16, 2026
c763102
fix(storybook): defer initial playground CSS compilation
dmytrokirpa Sep 16, 2026
fcdb59b
fix(storybook): address playground review feedback
dmytrokirpa Sep 16, 2026
9f7e864
fix(storybook): resolve exports-only typings
Copilot Sep 16, 2026
95b2ba9
test(storybook): clean typings fixture
Copilot Sep 16, 2026
cacbb81
Merge branch 'master' into experimental/storybook-playground
dmytrokirpa Sep 16, 2026
5ae34cb
fix: address playground review comments
Copilot Sep 17, 2026
a2b9d2d
fix: remove unused playground success state
Copilot Sep 17, 2026
0f5d13a
fix: improve playground runtime diagnostics
Copilot Sep 17, 2026
721f467
fix: harden playground runtime helpers
Copilot Sep 17, 2026
9d4d0d5
test: guard playground sandbox isolation
Copilot Sep 17, 2026
341c985
fix: enforce runtime module allowlist
Copilot Sep 17, 2026
a9fa478
chore: clarify sandbox helper internals
Copilot Sep 17, 2026
5f8b9d4
fix: preserve playground path during hash sync
Copilot Sep 17, 2026
29de0c5
test: cover exact module allowlists
Copilot Sep 17, 2026
5c6cdc0
fix: address playground review feedback
Copilot Sep 17, 2026
a4efdab
fix: keep playground run callbacks pure
Copilot Sep 17, 2026
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 apps/public-docsite-v9-headless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@fluentui/react-headless-components-preview": "*",
Comment thread
dmytrokirpa marked this conversation as resolved.
"@fluentui/react-storybook-addon": "*",
"@fluentui/react-storybook-addon-export-to-sandbox": "*",
"@fluentui/react-storybook-addon-playground": "*",
"react": "19.2.0",
"react-dom": "19.2.0",
"tslib": "^2.1.0"
Expand Down
16 changes: 14 additions & 2 deletions apps/public-docsite-v9-headless/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,27 @@
"build-storybook": {
"dependsOn": [
{
"projects": ["react-storybook-addon", "react-storybook-addon-export-to-sandbox", "storybook-llms-extractor"],
"projects": [
"react-headless-components-preview",
"react-storybook-addon",
"react-storybook-addon-export-to-sandbox",
"react-storybook-addon-playground",
"storybook-llms-extractor"
],
"target": "build"
}
]
},
"build-storybook:docsite": {
"dependsOn": [
{
"projects": ["react-storybook-addon", "react-storybook-addon-export-to-sandbox", "storybook-llms-extractor"],
"projects": [
"react-headless-components-preview",
"react-storybook-addon",
"react-storybook-addon-export-to-sandbox",
"react-storybook-addon-playground",
"storybook-llms-extractor"
],
"target": "build"
}
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "minor",
"comment": "feat: support per-story full source generation and playground addon buttons",
"packageName": "@fluentui/react-storybook-addon-export-to-sandbox",
"email": "dmytrokirpa@microsoft.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "minor",
"comment": "feat: add initial playground implementation",
"packageName": "@fluentui/react-storybook-addon-playground",
"email": "dmytrokirpa@microsoft.com"
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"type": "patch",
"comment": "feat: add @fluentui/react-storybook-addon-playground package scaffold",
"type": "minor",
"comment": "feat: add an in-browser TSX playground for Storybook stories",
"packageName": "@fluentui/react-storybook-addon-playground",
"email": "dmytrokirpa@microsoft.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '*.css';
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,79 @@ const {
processBabelLoaderOptions,
} = require('@fluentui/scripts-storybook');
const { registerCssModuleRules } = require('./css-modules-webpack');
const headlessPackageJson = require('../../library/package.json');

const repoRoot = path.resolve(__dirname, '../../../../..');
const tsConfigPath = path.resolve(repoRoot, 'tsconfig.base.json');
const HEADLESS_PACKAGE_NAME = '@fluentui/react-headless-components-preview';

/**
* Headless Storybook replaces the root sandbox addon with a CSS-modules-aware one, and registers
* the playground here only (not on the shared v9 Storybook).
*
* @param {string | { name?: string }} addon
*/
function isNotExportToSandboxAddon(addon) {
function isNotRootSandboxOrPlaygroundAddon(addon) {
const name = typeof addon === 'string' ? addon : addon?.name ?? '';
return !name.includes('react-storybook-addon-export-to-sandbox');
return (
!name.includes('react-storybook-addon-export-to-sandbox') && !name.includes('react-storybook-addon-playground')
);
}

/**
* Maps every published subpath of the headless package (plus react-icons) into the playground allowlist.
* Skips `.` — the barrel is empty, and Storybook only has a tsconfig path for `package/*` (source),
* not the package root (built `lib/index.js`, which CI `build-storybook` does not produce).
*
* @returns {Record<string, string>}
*/
function getHeadlessPlaygroundModules() {
/** @type {Record<string, string>} */
const modules = {
'@ctrl/tinycolor': '@ctrl/tinycolor',
'@fluentui/react-icons': '@fluentui/react-icons',
};
Comment thread
Copilot marked this conversation as resolved.

for (const exportPath of Object.keys(headlessPackageJson.exports ?? {})) {
if (exportPath === '.' || exportPath === './package.json') {
continue;
}

const specifier = `${HEADLESS_PACKAGE_NAME}${exportPath.slice(1)}`;
modules[specifier] = specifier;
}

return modules;
}

module.exports = /** @type {Omit<import('../../../../../.storybook/main'), 'typescript'|'babel'>} */ ({
...rootMain,
stories: [...rootMain.stories, '../src/**/*.mdx', '../src/**/index.stories.@(ts|tsx)'],
addons: [
...rootMain.addons.filter(isNotExportToSandboxAddon),
...rootMain.addons.filter(isNotRootSandboxOrPlaygroundAddon),
loadWorkspaceAddon('@fluentui/react-storybook-addon-export-to-sandbox', {
tsConfigPath,
/** @type {import('../../../react-storybook-addon-export-to-sandbox/src/index').PresetConfig} */
options: {
importMappings: getImportMappingsForExportToSandboxAddon(),
babelLoaderOptionsUpdater: processBabelLoaderOptions,
storyGranularity: 'story',
cssModules: { tokensFilePath: path.resolve(__dirname, 'tokens.css') },
webpackRule: {
test: /\.stories\.tsx$/,
include: /stories/,
},
},
}),
/** {@link file://./../../../react-storybook-addon-playground/package.json} */
loadWorkspaceAddon('@fluentui/react-storybook-addon-playground', {
tsConfigPath,
/** @type {import('../../../react-storybook-addon-playground/src/index').PresetConfig} */
options: {
modules: getHeadlessPlaygroundModules(),
setup: path.resolve(__dirname, './playground.setup.tsx'),
},
Comment on lines +75 to +81
}),
],
webpackFinal: (config, options) => {
const localConfig = /** @type {any} */ ({ ...rootMain.webpackFinal(config, options) });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react';
import { polyfillBodyAndObserve } from '@microsoft/focusgroup-polyfill/shadowless';
import { Provider } from '@fluentui/react-headless-components-preview/provider';
import { definePlaygroundSetup } from '@fluentui/react-storybook-addon-playground/setup';

import './tokens.css';

polyfillBodyAndObserve();

type HeadlessPlaygroundTheme = 'light' | 'dark';

const defaultCode = `import * as React from 'react';
import { Button } from '@fluentui/react-headless-components-preview/button';

export default () => {
const [count, setCount] = React.useState(0);

return (
<Button
onClick={() => setCount(count + 1)}
style={{
height: 32,
padding: '0 14px',
border: 0,
borderRadius: 999,
background: 'var(--accent)',
color: 'var(--accent-contrast)',
cursor: 'pointer',
}}
>
Clicked {count} times
</Button>
);
};
`;

export default definePlaygroundSetup<HeadlessPlaygroundTheme>({
title: 'Fluent UI Playground',
subtitle: 'Headless',
defaultCode,
themes: [
{ id: 'light', label: 'Light', value: 'light', dark: false },
{ id: 'dark', label: 'Dark', value: 'dark', dark: true },
],
render: ({ Component, theme }) => (
<Provider>
<div data-theme={theme ?? 'light'} style={{ minHeight: '100%', background: 'var(--bg)', color: 'var(--text)' }}>
<Component />
</div>
</Provider>
),
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { polyfillBodyAndObserve } from '@microsoft/focusgroup-polyfill/shadowless';
import '../../../react-storybook-addon-playground/src/styles.css';

import * as rootPreview from '../../../../../.storybook/preview';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"moduleResolution": "bundler",
"types": ["static-assets", "environment"]
},
"include": ["*.js"]
"include": ["*.js", "*.tsx", "*.d.ts"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,23 @@
"projectType": "library",
"sourceRoot": "packages/react-components/react-headless-components-preview/stories/src",
"tags": ["vNext", "platform:web", "type:stories", "react-headless"],
"implicitDependencies": []
"implicitDependencies": [],
"targets": {
"storybook": {
"dependsOn": [
{
"projects": ["react-headless-components-preview", "react-storybook-addon-playground"],
"target": "build"
}
]
},
"build-storybook": {
"dependsOn": [
{
"projects": ["react-headless-components-preview", "react-storybook-addon-playground"],
"target": "build"
}
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export default {
title: 'Concepts/Positioning',
component: Positioning,
parameters: {
playground: {
disable: true,
},
docs: {
description: {
component: [descriptionMd, bestPracticesMd].join('\n'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export default {
component: TagPicker,
subcomponents: { TagPickerControl, TagPickerGroup, TagPickerInput, TagPickerButton, TagPickerList, TagPickerOption },
parameters: {
playground: {
disable: true,
},
docs: {
description: {
component: [descriptionMd, getBrowserSupportNotice('TagPicker')].join('\n'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface PresetConfig {
// (undocumented)
importMappings: BabelPluginOptions['importMappings'];
// (undocumented)
storyGranularity?: BabelPluginOptions['storyGranularity'];
// (undocumented)
webpackRule?: RuleSetRule;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface ParametersExtension {

export interface PresetConfig {
importMappings: import('@fluentui/babel-preset-storybook-full-source').BabelPluginOptions['importMappings'];
storyGranularity?: import('@fluentui/babel-preset-storybook-full-source').BabelPluginOptions['storyGranularity'];
webpackRule?: import('webpack').RuleSetRule;
babelLoaderOptionsUpdater?: (value: import('@babel/core').TransformOptions) => typeof value;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ export function prepareSandboxContainers(context: StoryContext) {
}

return Array.from(rootElements).map(rootElement => {
// Exclude buttons added by this addon and by @fluentui/react-storybook-addon-playground
const showCodeButton = rootElement.querySelector(
'.docblock-code-toggle:not(.with-code-sandbox-button):not(.with-open-in-new-tab-button)',
'.docblock-code-toggle:not(.with-code-sandbox-button):not(.with-open-in-new-tab-button):not(.with-open-in-playground-button)',
);
const container = showCodeButton?.parentElement;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe(`webpack`, () => {
plugins: [
[
expect.stringContaining('babel-preset-storybook-full-source'),
{ importMappings: undefined, cssModules: false },
{ importMappings: undefined, storyGranularity: 'file', cssModules: false },
],
],
},
Expand All @@ -46,6 +46,7 @@ describe(`webpack`, () => {
babelLoaderOptionsUpdater: value => {
return Object.assign(value, { presets: ['babel-foo-bar-preset'] });
},
storyGranularity: 'story',
} as PresetConfig,
},
],
Expand All @@ -65,6 +66,7 @@ describe(`webpack`, () => {
expect.stringContaining('babel-preset-storybook-full-source'),
{
importMappings: { '@proj/foo': { replace: '@proj/moo' } },
storyGranularity: 'story',
cssModules: false,
},
],
Expand Down Expand Up @@ -101,7 +103,7 @@ describe(`webpack`, () => {
plugins: [
[
expect.stringContaining('babel-preset-storybook-full-source'),
{ importMappings: undefined, cssModules },
{ importMappings: undefined, storyGranularity: 'file', cssModules },
],
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const addonFilePattern = /react-storybook-addon-export-to-sandbox\/[a-z/]+.[jt]s
const defaultOptions = {
webpackRule: {},
babelLoaderOptionsUpdater: identity,
storyGranularity: 'file' as const,
cssModules: false,
};

Expand All @@ -29,9 +30,9 @@ const PLUGIN_PATH =
: '@fluentui/babel-preset-storybook-full-source';

function createBabelLoaderRule(config: Required<PresetConfig>): import('webpack').RuleSetRule {
const { babelLoaderOptionsUpdater, importMappings, webpackRule, cssModules } = config;
const { babelLoaderOptionsUpdater, importMappings, webpackRule, storyGranularity, cssModules } = config;

const plugin = [require.resolve(PLUGIN_PATH), { importMappings, cssModules }];
const plugin = [require.resolve(PLUGIN_PATH), { importMappings, storyGranularity, cssModules }];

return {
test: /\.stories\.(jsx?$|tsx?$)/,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"/**/*.spec.ts",
"/**/*.spec.tsx",
"/**/*.test.ts",
"/**/*.test.tsx"
"/**/*.test.tsx",
"/**/src/playground/**"
],
"jsc": {
"baseUrl": ".",
Expand Down
Loading