Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement docsite for contrib packages #244

Merged
merged 14 commits into from
Oct 22, 2024
Merged
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#### Applicaitions
apps/docsite @microsoft/cxe-prg

#### Packages
packages/react-chat/ @microsoft/teams-prg
packages/react-data-grid-react-window/ @microsoft/teams-prg
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';
import { DocsContainer, type DocsContextProps } from '@storybook/addon-docs';
import { webLightTheme, FluentProvider } from '@fluentui/react-components';

interface FluentDocsContainerProps {
children: React.ReactNode;
context: DocsContextProps;
}

/**
* A container that wraps storybook's native docs container to add extra components to the docs experience
*/
export const FluentDocsContainer = ({
children,
context,
}: FluentDocsContainerProps) => {
return (
<FluentProvider
className="sb-unstyled"
style={{ backgroundColor: 'transparent' }}
theme={webLightTheme}
>
<DocsContainer context={context}>{children}</DocsContainer>
</FluentProvider>
);
};
159 changes: 159 additions & 0 deletions .storybook/copied-from-fluentui-core/components/FluentDocsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as React from 'react';
import {
DocsContext,
ArgsTable,
Description,
Stories,
type DocsContextProps,
} from '@storybook/addon-docs';
import type { SBEnumType, PreparedStory, Renderer } from '@storybook/types';
import { makeStyles, shorthands, tokens } from '@fluentui/react-components';
import { InfoFilled } from '@fluentui/react-icons';
import { Toc } from './Toc';

type PrimaryStory = PreparedStory<Renderer>;

const useStyles = makeStyles({
divider: {
height: '1px',
backgroundColor: '#e1dfdd',
...shorthands.border('0px', 'none'),
...shorthands.margin('48px', '0px'),
},
wrapper: {
display: 'flex',
...shorthands.gap('16px'),
},
toc: {
flexBasis: '200px',
flexShrink: 0,
[`@media screen and (max-width: 1000px)`]: {
display: 'none',
},
},
container: {
// without a width, this div grows wider than its parent
width: '200px',
flexGrow: 1,
},
globalTogglesContainer: {
columnGap: tokens.spacingHorizontalXXXL,
display: 'flex',
},
description: {
display: 'grid',
gridTemplateColumns: '1fr min-content',
},
nativeProps: {
display: 'flex',
gap: tokens.spacingHorizontalM,

border: `1px solid ${tokens.colorNeutralStroke1}`,
borderRadius: tokens.borderRadiusMedium,
padding: tokens.spacingHorizontalM,
margin: `0 ${tokens.spacingHorizontalM}`,
},
nativePropsIcon: {
alignSelf: 'center',
color: tokens.colorBrandForeground1,
fontSize: '24px',
},
nativePropsMessage: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalXS,
},
});

const getNativeElementsList = (elements: SBEnumType['value']): JSX.Element => {
const elementsArr = elements.map((el, idx) => [
<code key={idx}>{`<${el}>`}</code>,
idx !== elements.length - 1 ? ', ' : ' ',
]);

return (
<>
{elementsArr}
{elementsArr.length > 1 ? 'elements' : 'element'}
</>
);
};

const RenderArgsTable = ({
hideArgsTable,
primaryStory,
}: {
primaryStory: PrimaryStory;
hideArgsTable: boolean;
}) => {
const styles = useStyles();
return hideArgsTable ? null : (
<>
<ArgsTable of={primaryStory.component} />
{primaryStory.argTypes.as &&
primaryStory.argTypes.as?.type?.name === 'enum' && (
<div className={styles.nativeProps}>
<InfoFilled className={styles.nativePropsIcon} />
<div className={styles.nativePropsMessage}>
<b>
Native props are supported <span role="presentation">🙌</span>
</b>
<span>
All HTML attributes native to the{' '}
{getNativeElementsList(primaryStory.argTypes.as.type.value)},
including all <code>aria-*</code> and <code>data-*</code>{' '}
attributes, can be applied as native props on this component.
</span>
</div>
</div>
)}
</>
);
};

export const FluentDocsPage = () => {
const context = React.useContext(DocsContext);
const stories = context.componentStories();
const primaryStory = stories[0];
const primaryStoryContext = context.getStoryContext(primaryStory);

assertStoryMetaValues(primaryStory);

const hideArgsTable = Boolean(
primaryStoryContext.parameters?.docs?.hideArgsTable
);

const styles = useStyles();

return (
<div className="sb-unstyled">
<div className={styles.wrapper}>
<div className={styles.container}>
<Description />
<RenderArgsTable
primaryStory={primaryStory}
hideArgsTable={hideArgsTable}
/>
<Stories title="Examples" />
</div>
<div className={styles.toc}>
<Toc stories={stories} />
</div>
</div>
</div>
);
};

function assertStoryMetaValues(
story: ReturnType<DocsContextProps['componentStories']>[number]
) {
if (story.component === null) {
throw new Error(
[
'🚨 Invalid Story Meta declaration:',
`- primaryStory.component of componentId:${story.componentId} is "null"`,
'- to resolve this error, please update "component" property value in your story definition to reference a React Component or remove it if it is not needed.',
].join('\n')
);
}
}
142 changes: 142 additions & 0 deletions .storybook/copied-from-fluentui-core/components/Toc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import * as React from 'react';
import { addons } from '@storybook/preview-api';
import { NAVIGATE_URL } from '@storybook/core-events';
import { makeStyles, shorthands } from '@fluentui/react-components';

const useTocStyles = makeStyles({
root: {
top: '64px',
position: 'sticky',
marginLeft: '40px',
},
heading: {
fontSize: '11px',
fontWeight: 'bold',
textTransform: 'uppercase',
marginBottom: '20px',
},
ol: {
position: 'relative',
listStyleType: 'none',
marginLeft: 0,
marginTop: 0,
paddingInlineStart: '20px',
'& li': {
marginBottom: '15px',
lineHeight: '16px',
},
'& a': {
textDecorationLine: 'none',
color: '#201F1E',
fontSize: '14px',
':hover': {
color: '#201F1E',
},
},
'&:before': {
content: '""',
position: 'absolute',
left: 0,
height: '100%',
width: '3px',
backgroundColor: '#EDEBE9',
...shorthands.borderRadius('4px'),
},
},
selected: {
position: 'relative',
'&:after': {
content: '""',
position: 'absolute',
left: '-20px',
top: 0,
bottom: 0,
width: '3px',
backgroundColor: '#436DCD',
...shorthands.borderRadius('4px'),
},
},
});

type TocItem = { name: string; id: string; selected?: boolean };

// // Alternative approach to navigate - rerenders the iframe
// // Usage: selectStory({ story: s.name, kind: s.kind });
// const selectStory = (story: { kind: string; story: string }) => {
// console.log('Select Story', story);
// addons.getChannel().emit(SELECT_STORY, story);
// };

const navigate = (url: string) => {
addons.getChannel().emit(NAVIGATE_URL, url);
};

export const nameToHash = (id: string): string =>
id.toLowerCase().replace(/[^a-z0-9]/gi, '-');

export const Toc = ({ stories }: { stories: TocItem[] }) => {
const [selected, setSelected] = React.useState('');
const isNavigating = React.useRef<boolean>(false);

React.useEffect(() => {
// eslint-disable-next-line no-restricted-globals
const observer = new IntersectionObserver(
(entries: IntersectionObserverEntry[]) => {
if (isNavigating.current) {
isNavigating.current = false;
return;
}
for (const entry of entries) {
const { intersectionRatio, target } = entry;
if (intersectionRatio > 0.5) {
setSelected(target.id);
return;
}
}
},
{
threshold: [0.5],
}
);

stories.forEach((link) => {
// eslint-disable-next-line no-restricted-globals
const element = document.getElementById(nameToHash(link.name));
if (element) {
observer.observe(element);
}
});

return () => observer.disconnect();
}, [stories]);

const tocItems = stories.map((item) => {
return { ...item, selected: nameToHash(item.name) === selected };
});
const tocClasses = useTocStyles();
return (
<nav className={tocClasses.root}>
<h3 className={tocClasses.heading}>On this page</h3>
<ol className={tocClasses.ol}>
{tocItems.map((s) => {
const name = nameToHash(s.name);
return (
<li className={s.selected ? tocClasses.selected : ''} key={s.id}>
<a
href={`#${name}`}
target="_self"
onClick={(e) => {
isNavigating.current = true;
navigate(`#${name}`);
setSelected(name);
}}
>
{s.name}
</a>
</li>
);
})}
</ol>
</nav>
);
};
23 changes: 23 additions & 0 deletions .storybook/copied-from-fluentui-core/docs-root-v9.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* remove the docs wrapper bg to let page bg show through */
/* remove unnecessary padding now that theme switcher is not taking up space */
#storybook-docs .sbdocs-wrapper {
background: transparent !important;
padding: 0 48px;
}

/* sb-show-main is missing during page transitions causing a page shift */
/* todo: cleanup once we no longer inherit docs-root */
.sb-show-main.sb-main-fullscreen,
.sb-main-fullscreen {
margin: 0;
padding: 0;
display: block;
}

/* remove loading overlay */
.sb-preparing-story,
.sb-preparing-docs,
.sb-nopreview,
.sb-errordisplay {
display: none !important;
}
Loading