Skip to content

Commit

Permalink
feat: implement docsite for contrib packages (#244)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmytrokirpa authored Oct 22, 2024
1 parent 184e510 commit 5f2bc3a
Show file tree
Hide file tree
Showing 84 changed files with 1,454 additions and 566 deletions.
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#### Applications
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

0 comments on commit 5f2bc3a

Please sign in to comment.