mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Feat: Introduce Button and LinkButton components to @grafana/ui (#16228)
- Bumped Storybook to v5 - Introduced Emotion - Add additional config for storybook (combinations add-on, default padding in preview pane) - Added basic react based button components - Introduced AbstractButton, Button and LinkButton components together with stories - Exposed button components from @grafana/ui
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { configure, addDecorator } from '@storybook/react';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
import { withTheme } from '../src/utils/storybook/withTheme';
|
||||
import { withPaddedStory } from '../src/utils/storybook/withPaddedStory';
|
||||
|
||||
// @ts-ignore
|
||||
import lightTheme from '../../../public/sass/grafana.light.scss';
|
||||
@@ -20,6 +21,7 @@ const handleThemeChange = (theme: string) => {
|
||||
const req = require.context('../src/components', true, /.story.tsx$/);
|
||||
|
||||
addDecorator(withKnobs);
|
||||
addDecorator(withPaddedStory);
|
||||
addDecorator(withTheme(handleThemeChange));
|
||||
|
||||
function loadStories() {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = (baseConfig, env, config) => {
|
||||
module.exports = ({config, mode}) => {
|
||||
config.module.rules.push({
|
||||
test: /\.(ts|tsx)$/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('awesome-typescript-loader'),
|
||||
options: {
|
||||
configFileName: path.resolve(__dirname+'/../tsconfig.json')
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -56,9 +59,5 @@ module.exports = (baseConfig, env, config) => {
|
||||
});
|
||||
|
||||
config.resolve.extensions.push('.ts', '.tsx');
|
||||
|
||||
// Remove pure js loading rules as Storybook's Babel config is causing problems when mixing ES6 and CJS
|
||||
// More about the problem we encounter: https://github.com/webpack/webpack/issues/4039
|
||||
config.module.rules = config.module.rules.filter(rule => rule.test.toString() !== /\.(mjs|jsx?)$/.toString());
|
||||
return config;
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"react-dom": "^16.8.4",
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-popper": "^1.3.0",
|
||||
"react-storybook-addon-props-combinations": "^1.1.0",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"react-virtualized": "^9.21.0",
|
||||
"tether": "^1.4.0",
|
||||
@@ -39,10 +40,11 @@
|
||||
"tinycolor2": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^4.1.7",
|
||||
"@storybook/addon-info": "^4.1.6",
|
||||
"@storybook/addon-knobs": "^4.1.7",
|
||||
"@storybook/react": "^4.1.4",
|
||||
"@storybook/addon-actions": "^5.0.5",
|
||||
"@storybook/addon-info": "^5.0.5",
|
||||
"@storybook/addon-knobs": "^5.0.5",
|
||||
"@storybook/react": "^5.0.5",
|
||||
"@storybook/theming": "^5.0.5",
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/d3": "^5.7.0",
|
||||
"@types/jest": "^23.3.2",
|
||||
@@ -50,17 +52,19 @@
|
||||
"@types/lodash": "^4.14.119",
|
||||
"@types/node": "^10.12.18",
|
||||
"@types/papaparse": "^4.5.9",
|
||||
"@types/pretty-format": "^20.0.1",
|
||||
"@types/react": "^16.8.8",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@types/react-test-renderer": "^16.0.3",
|
||||
"@types/react-transition-group": "^2.0.15",
|
||||
"@types/storybook__addon-actions": "^3.4.1",
|
||||
"@types/storybook__addon-info": "^3.4.2",
|
||||
"@types/storybook__addon-info": "^4.1.1",
|
||||
"@types/storybook__addon-knobs": "^4.0.0",
|
||||
"@types/storybook__react": "^4.0.0",
|
||||
"@types/tether-drop": "^1.4.8",
|
||||
"@types/tinycolor2": "^1.4.1",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"pretty-format": "^24.5.0",
|
||||
"react-docgen-typescript-loader": "^3.0.0",
|
||||
"react-docgen-typescript-webpack-plugin": "^1.1.0",
|
||||
"react-test-renderer": "^16.7.0",
|
||||
|
||||
212
packages/grafana-ui/src/components/Button/AbstractButton.tsx
Normal file
212
packages/grafana-ui/src/components/Button/AbstractButton.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { css, cx } from 'emotion';
|
||||
import { Themeable, GrafanaTheme } from '../../types';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
|
||||
export enum ButtonVariant {
|
||||
Primary = 'primary',
|
||||
Secondary = 'secondary',
|
||||
Danger = 'danger',
|
||||
Inverse = 'inverse',
|
||||
Transparent = 'transparent',
|
||||
}
|
||||
|
||||
export enum ButtonSize {
|
||||
ExtraSmall = 'xs',
|
||||
Small = 'sm',
|
||||
Medium = 'md',
|
||||
Large = 'lg',
|
||||
ExtraLarge = 'xl',
|
||||
}
|
||||
|
||||
export interface CommonButtonProps {
|
||||
size?: ButtonSize;
|
||||
variant?: ButtonVariant;
|
||||
/**
|
||||
* icon prop is a temporary solution. It accepts lefacy icon class names for the icon to be rendered.
|
||||
* TODO: migrate to a component when we are going to migrate icons to @grafana/ui
|
||||
*/
|
||||
icon?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface LinkButtonProps extends CommonButtonProps, AnchorHTMLAttributes<HTMLAnchorElement> {}
|
||||
export interface ButtonProps extends CommonButtonProps, ButtonHTMLAttributes<HTMLButtonElement> {}
|
||||
|
||||
interface AbstractButtonProps extends CommonButtonProps, Themeable {
|
||||
renderAs: React.ComponentType<CommonButtonProps> | string;
|
||||
}
|
||||
|
||||
const buttonVariantStyles = (
|
||||
from: string,
|
||||
to: string,
|
||||
textColor: string,
|
||||
textShadowColor = 'rgba(0, 0, 0, 0.1)',
|
||||
invert = false
|
||||
) => css`
|
||||
background: linear-gradient(to bottom, ${from}, ${to});
|
||||
color: ${textColor};
|
||||
text-shadow: 0 ${invert ? '1px' : '-1px'} ${textShadowColor};
|
||||
&:hover {
|
||||
background: ${from};
|
||||
color: ${textColor};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: ${from};
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const getButtonStyles = (theme: GrafanaTheme, size: ButtonSize, variant: ButtonVariant, withIcon: boolean) => {
|
||||
const borderRadius = theme.border.radius.sm;
|
||||
let padding,
|
||||
background,
|
||||
fontSize,
|
||||
iconDistance,
|
||||
fontWeight = theme.typography.weight.semibold;
|
||||
|
||||
switch (size) {
|
||||
case ButtonSize.ExtraSmall:
|
||||
padding = `${theme.spacing.xs} ${theme.spacing.sm}`;
|
||||
fontSize = theme.typography.size.xs;
|
||||
iconDistance = theme.spacing.xs;
|
||||
break;
|
||||
case ButtonSize.Small:
|
||||
padding = `${theme.spacing.xs} ${theme.spacing.sm}`;
|
||||
fontSize = theme.typography.size.sm;
|
||||
iconDistance = theme.spacing.xs;
|
||||
break;
|
||||
case ButtonSize.Large:
|
||||
padding = `${theme.spacing.md} ${theme.spacing.lg}`;
|
||||
fontSize = theme.typography.size.lg;
|
||||
fontWeight = theme.typography.weight.regular;
|
||||
iconDistance = theme.spacing.sm;
|
||||
break;
|
||||
case ButtonSize.ExtraLarge:
|
||||
padding = `${theme.spacing.md} ${theme.spacing.lg}`;
|
||||
fontSize = theme.typography.size.lg;
|
||||
fontWeight = theme.typography.weight.regular;
|
||||
iconDistance = theme.spacing.sm;
|
||||
break;
|
||||
default:
|
||||
padding = `${theme.spacing.sm} ${theme.spacing.md}`;
|
||||
iconDistance = theme.spacing.sm;
|
||||
fontSize = theme.typography.size.base;
|
||||
}
|
||||
|
||||
switch (variant) {
|
||||
case ButtonVariant.Primary:
|
||||
background = buttonVariantStyles(theme.colors.greenBase, theme.colors.greenShade, theme.colors.white);
|
||||
break;
|
||||
case ButtonVariant.Secondary:
|
||||
background = buttonVariantStyles(theme.colors.blueBase, theme.colors.blueShade, theme.colors.white);
|
||||
break;
|
||||
case ButtonVariant.Danger:
|
||||
background = buttonVariantStyles(theme.colors.redBase, theme.colors.redShade, theme.colors.white);
|
||||
break;
|
||||
case ButtonVariant.Inverse:
|
||||
const from = selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.dark6 }, theme.type) as string;
|
||||
const to = selectThemeVariant(
|
||||
{
|
||||
light: tinycolor(from)
|
||||
.darken(5)
|
||||
.toString(),
|
||||
dark: tinycolor(from)
|
||||
.lighten(4)
|
||||
.toString(),
|
||||
},
|
||||
theme.type
|
||||
) as string;
|
||||
|
||||
background = buttonVariantStyles(from, to, theme.colors.link, 'rgba(0, 0, 0, 0.1)', true);
|
||||
break;
|
||||
case ButtonVariant.Transparent:
|
||||
background = css`
|
||||
${buttonVariantStyles('', '', theme.colors.link, 'rgba(0, 0, 0, 0.1)', true)};
|
||||
background: transparent;
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
button: css`
|
||||
label: button;
|
||||
display: inline-block;
|
||||
font-weight: ${fontWeight};
|
||||
font-size: ${fontSize};
|
||||
font-family: ${theme.typography.fontFamily.sansSerif};
|
||||
line-height: ${theme.typography.lineHeight.xs};
|
||||
padding: ${padding};
|
||||
text-align: ${withIcon ? 'left' : 'center'};
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: ${borderRadius};
|
||||
${background};
|
||||
|
||||
&[disabled],
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
box-shadow: none;
|
||||
}
|
||||
`,
|
||||
iconWrap: css`
|
||||
label: button-icon-wrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
icon: css`
|
||||
label: button-icon;
|
||||
margin-right: ${iconDistance};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
|
||||
renderAs,
|
||||
theme,
|
||||
size = ButtonSize.Medium,
|
||||
variant = ButtonVariant.Primary,
|
||||
className,
|
||||
icon,
|
||||
children,
|
||||
...otherProps
|
||||
}) => {
|
||||
const buttonStyles = getButtonStyles(theme, size, variant, !!icon);
|
||||
const nonHtmlProps = {
|
||||
theme,
|
||||
size,
|
||||
variant,
|
||||
};
|
||||
|
||||
const finalClassName = cx(buttonStyles.button, className);
|
||||
const finalChildren = icon ? (
|
||||
<span className={buttonStyles.iconWrap}>
|
||||
<i className={cx([icon, buttonStyles.icon])} />
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
|
||||
const finalProps =
|
||||
typeof renderAs === 'string'
|
||||
? {
|
||||
...otherProps,
|
||||
className: finalClassName,
|
||||
children: finalChildren,
|
||||
}
|
||||
: {
|
||||
...otherProps,
|
||||
...nonHtmlProps,
|
||||
className: finalClassName,
|
||||
children: finalChildren,
|
||||
};
|
||||
|
||||
return React.createElement(renderAs, finalProps);
|
||||
};
|
||||
|
||||
AbstractButton.displayName = 'AbstractButton';
|
||||
56
packages/grafana-ui/src/components/Button/Button.story.tsx
Normal file
56
packages/grafana-ui/src/components/Button/Button.story.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { Button, LinkButton } from './Button';
|
||||
import { ButtonSize, ButtonVariant, CommonButtonProps } from './AbstractButton';
|
||||
// @ts-ignore
|
||||
import withPropsCombinations from 'react-storybook-addon-props-combinations';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ThemeableCombinationsRowRenderer } from '../../utils/storybook/CombinationsRowRenderer';
|
||||
import { select, boolean } from '@storybook/addon-knobs';
|
||||
|
||||
const ButtonStories = storiesOf('UI/Button', module);
|
||||
|
||||
const defaultProps = {
|
||||
onClick: [action('Button clicked')],
|
||||
children: ['Click, click!'],
|
||||
};
|
||||
|
||||
const variants = {
|
||||
size: [ButtonSize.ExtraSmall, ButtonSize.Small, ButtonSize.Medium, ButtonSize.Large, ButtonSize.ExtraLarge],
|
||||
variant: [
|
||||
ButtonVariant.Primary,
|
||||
ButtonVariant.Secondary,
|
||||
ButtonVariant.Danger,
|
||||
ButtonVariant.Inverse,
|
||||
ButtonVariant.Transparent,
|
||||
],
|
||||
};
|
||||
const combinationOptions = {
|
||||
CombinationRenderer: ThemeableCombinationsRowRenderer,
|
||||
};
|
||||
|
||||
const renderButtonStory = (buttonComponent: React.ComponentType<CommonButtonProps>) => {
|
||||
const isDisabled = boolean('Disable button', false);
|
||||
return withPropsCombinations(
|
||||
buttonComponent,
|
||||
{ ...variants, ...defaultProps, disabled: [isDisabled] },
|
||||
combinationOptions
|
||||
)();
|
||||
};
|
||||
|
||||
ButtonStories.add('as button element', () => renderButtonStory(Button));
|
||||
|
||||
ButtonStories.add('as link element', () => renderButtonStory(LinkButton));
|
||||
|
||||
ButtonStories.add('with icon', () => {
|
||||
const iconKnob = select(
|
||||
'Icon',
|
||||
{
|
||||
Plus: 'fa fa-plus',
|
||||
User: 'fa fa-user',
|
||||
Gear: 'fa fa-gear',
|
||||
Annotation: 'gicon gicon-add-annotation',
|
||||
},
|
||||
'fa fa-plus'
|
||||
);
|
||||
return withPropsCombinations(Button, { ...variants, ...defaultProps, icon: [iconKnob] }, combinationOptions)();
|
||||
});
|
||||
86
packages/grafana-ui/src/components/Button/Button.tsx
Normal file
86
packages/grafana-ui/src/components/Button/Button.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { AbstractButton, ButtonProps, ButtonSize, LinkButtonProps } from './AbstractButton';
|
||||
import { ThemeContext } from '../../themes';
|
||||
|
||||
const getSizeNameComponentSegment = (size: ButtonSize) => {
|
||||
switch (size) {
|
||||
case ButtonSize.ExtraSmall:
|
||||
return 'ExtraSmall';
|
||||
case ButtonSize.Small:
|
||||
return 'Small';
|
||||
case ButtonSize.Large:
|
||||
return 'Large';
|
||||
case ButtonSize.ExtraLarge:
|
||||
return 'ExtraLarge';
|
||||
default:
|
||||
return 'Medium';
|
||||
}
|
||||
};
|
||||
|
||||
const buttonFactory: <T>(renderAs: string, size: ButtonSize, displayName: string) => React.ComponentType<T> = (
|
||||
renderAs,
|
||||
size,
|
||||
displayName
|
||||
) => {
|
||||
const ButtonComponent: React.FunctionComponent<any> = props => {
|
||||
const theme = useContext(ThemeContext);
|
||||
return <AbstractButton {...props} size={size} renderAs={renderAs} theme={theme} />;
|
||||
};
|
||||
ButtonComponent.displayName = displayName;
|
||||
|
||||
return ButtonComponent;
|
||||
};
|
||||
|
||||
export const Button: React.FunctionComponent<ButtonProps> = props => {
|
||||
const theme = useContext(ThemeContext);
|
||||
return <AbstractButton {...props} renderAs="button" theme={theme} />;
|
||||
};
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export const LinkButton: React.FunctionComponent<LinkButtonProps> = props => {
|
||||
const theme = useContext(ThemeContext);
|
||||
return <AbstractButton {...props} renderAs="a" theme={theme} />;
|
||||
};
|
||||
LinkButton.displayName = 'LinkButton';
|
||||
|
||||
export const ExtraSmallButton = buttonFactory<ButtonProps>(
|
||||
'button',
|
||||
ButtonSize.ExtraSmall,
|
||||
`${getSizeNameComponentSegment(ButtonSize.ExtraSmall)}Button`
|
||||
);
|
||||
export const SmallButton = buttonFactory<ButtonProps>(
|
||||
'button',
|
||||
ButtonSize.Small,
|
||||
`${getSizeNameComponentSegment(ButtonSize.Small)}Button`
|
||||
);
|
||||
export const LargeButton = buttonFactory<ButtonProps>(
|
||||
'button',
|
||||
ButtonSize.Large,
|
||||
`${getSizeNameComponentSegment(ButtonSize.Large)}Button`
|
||||
);
|
||||
export const ExtraLargeButton = buttonFactory<ButtonProps>(
|
||||
'button',
|
||||
ButtonSize.ExtraLarge,
|
||||
`${getSizeNameComponentSegment(ButtonSize.ExtraLarge)}Button`
|
||||
);
|
||||
|
||||
export const ExtraSmallLinkButton = buttonFactory<LinkButtonProps>(
|
||||
'a',
|
||||
ButtonSize.ExtraSmall,
|
||||
`${getSizeNameComponentSegment(ButtonSize.ExtraSmall)}LinkButton`
|
||||
);
|
||||
export const SmallLinkButton = buttonFactory<LinkButtonProps>(
|
||||
'a',
|
||||
ButtonSize.Small,
|
||||
`${getSizeNameComponentSegment(ButtonSize.Small)}LinkButton`
|
||||
);
|
||||
export const LargeLinkButton = buttonFactory<LinkButtonProps>(
|
||||
'a',
|
||||
ButtonSize.Large,
|
||||
`${getSizeNameComponentSegment(ButtonSize.Large)}LinkButton`
|
||||
);
|
||||
export const ExtraLargeLinkButton = buttonFactory<LinkButtonProps>(
|
||||
'a',
|
||||
ButtonSize.ExtraLarge,
|
||||
`${getSizeNameComponentSegment(ButtonSize.ExtraLarge)}LinkButton`
|
||||
);
|
||||
@@ -5,6 +5,8 @@ export { Popper } from './Tooltip/Popper';
|
||||
export { Portal } from './Portal/Portal';
|
||||
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
||||
|
||||
export * from './Button/Button';
|
||||
|
||||
// Select
|
||||
export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
|
||||
export { IndicatorsContainer } from './Select/IndicatorsContainer';
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { withTheme } from '../../themes';
|
||||
import { Themeable } from '../../types';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
import prettyFormat from 'pretty-format';
|
||||
|
||||
const detailsRenderer: (combinationProps: any) => JSX.Element = props => {
|
||||
const listStyle = css`
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
`;
|
||||
|
||||
return (
|
||||
<ul className={listStyle}>
|
||||
<li>
|
||||
{Object.keys(props).map((key, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
{key}: {props[key]}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
interface CombinationsRowRendererProps extends Themeable {
|
||||
Component: React.ComponentType<any>;
|
||||
props: any;
|
||||
options: any;
|
||||
}
|
||||
|
||||
const CombinationsRowRenderer: React.FunctionComponent<CombinationsRowRendererProps> = ({
|
||||
Component,
|
||||
props,
|
||||
theme,
|
||||
}) => {
|
||||
const el = React.createElement(Component, props);
|
||||
|
||||
const borderColor = selectThemeVariant(
|
||||
{
|
||||
dark: theme.colors.dark8,
|
||||
light: theme.colors.gray5,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
const rowStyle = css`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
border: 1px solid ${borderColor};
|
||||
border-bottom: none;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
}
|
||||
`;
|
||||
const cellStyle = css`
|
||||
padding: 10px;
|
||||
`;
|
||||
const previewCellStyle = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
flex-shrink: 1;
|
||||
border-right: 1px solid ${borderColor};
|
||||
${cellStyle};
|
||||
`;
|
||||
const variantsCellStyle = css`
|
||||
width: 200px;
|
||||
border-right: 1px solid ${borderColor};
|
||||
${cellStyle};
|
||||
`;
|
||||
|
||||
return (
|
||||
<div className={rowStyle}>
|
||||
<div className={previewCellStyle}>{el}</div>
|
||||
<div className={variantsCellStyle}>{detailsRenderer(props)}</div>
|
||||
<div className={cellStyle}>
|
||||
{prettyFormat(el, {
|
||||
plugins: [prettyFormat.plugins.ReactElement],
|
||||
printFunctionName: true,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThemeableCombinationsRowRenderer = withTheme(CombinationsRowRenderer);
|
||||
16
packages/grafana-ui/src/utils/storybook/withPaddedStory.tsx
Normal file
16
packages/grafana-ui/src/utils/storybook/withPaddedStory.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { RenderFunction } from '@storybook/react';
|
||||
|
||||
const PaddedStory: React.FunctionComponent<{}> = ({ children }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const withPaddedStory = (story: RenderFunction) => <PaddedStory>{story()}</PaddedStory>;
|
||||
Reference in New Issue
Block a user