feat(component): add storybook (#5079)

This commit is contained in:
Cats Juice
2023-12-04 08:32:19 +00:00
parent 9c50dbc362
commit d911d21d1c
30 changed files with 1640 additions and 50 deletions

View File

@@ -0,0 +1,52 @@
import { CameraIcon } from '@blocksuite/icons';
import type { Meta, StoryFn } from '@storybook/react';
import { Avatar, type AvatarProps } from './avatar';
export default {
title: 'UI/Avatar',
component: Avatar,
argTypes: {
onClick: () => console.log('Click button'),
},
} satisfies Meta<AvatarProps>;
const Template: StoryFn<AvatarProps> = args => <Avatar {...args} />;
export const DefaultAvatar: StoryFn<AvatarProps> = Template.bind(undefined);
DefaultAvatar.args = {
name: 'AFFiNE',
url: 'https://affine.pro/favicon-96.png',
size: 50,
};
export const Fallback: StoryFn<AvatarProps> = Template.bind(undefined);
Fallback.args = {
name: 'AFFiNE',
size: 50,
};
export const ColorfulFallback: StoryFn<AvatarProps> = Template.bind(undefined);
ColorfulFallback.args = {
size: 50,
colorfulFallback: true,
name: 'blocksuite',
};
export const WithHover: StoryFn<AvatarProps> = Template.bind(undefined);
WithHover.args = {
size: 50,
colorfulFallback: true,
name: 'With Hover',
hoverIcon: <CameraIcon />,
};
export const WithRemove: StoryFn<AvatarProps> = Template.bind(undefined);
WithRemove.args = {
size: 50,
colorfulFallback: true,
name: 'With Hover',
hoverIcon: <CameraIcon />,
removeTooltipOptions: { content: 'This is remove tooltip' },
avatarTooltipOptions: { content: 'This is avatar tooltip' },
onRemove: e => {
console.log('on remove', e);
},
};

View File

@@ -0,0 +1,46 @@
import { InformationIcon } from '@blocksuite/icons';
import type { Meta, StoryFn } from '@storybook/react';
import { Button, type ButtonProps } from './button';
export default {
title: 'UI/Button',
component: Button,
argTypes: {
onClick: () => console.log('Click button'),
},
} satisfies Meta<ButtonProps>;
const Template: StoryFn<ButtonProps> = args => <Button {...args} />;
export const Default: StoryFn<ButtonProps> = Template.bind(undefined);
Default.args = {
type: 'default',
children: 'This is a default button',
icon: <InformationIcon />,
};
export const Primary: StoryFn<ButtonProps> = Template.bind(undefined);
Primary.args = {
type: 'primary',
children: 'Content',
icon: <InformationIcon />,
};
export const Disabled: StoryFn<ButtonProps> = Template.bind(undefined);
Disabled.args = {
disabled: true,
children: 'This is a disabled button',
};
export const LargeSizeButton: StoryFn<ButtonProps> = Template.bind(undefined);
LargeSizeButton.args = {
size: 'large',
children: 'This is a large button',
};
export const ExtraLargeSizeButton: StoryFn<ButtonProps> =
Template.bind(undefined);
ExtraLargeSizeButton.args = {
size: 'extraLarge',
children: 'This is a extra large button',
};

View File

@@ -0,0 +1,48 @@
import { InformationIcon } from '@blocksuite/icons';
import type { Meta, StoryFn } from '@storybook/react';
import { IconButton, type IconButtonProps } from './icon-button';
export default {
title: 'UI/IconButton',
component: IconButton,
argTypes: {
onClick: () => console.log('Click button'),
},
} satisfies Meta<IconButtonProps>;
const Template: StoryFn<IconButtonProps> = args => <IconButton {...args} />;
export const Plain: StoryFn<IconButtonProps> = Template.bind(undefined);
Plain.args = {
children: <InformationIcon />,
};
export const Primary: StoryFn<IconButtonProps> = Template.bind(undefined);
Primary.args = {
type: 'primary',
icon: <InformationIcon />,
};
export const Disabled: StoryFn<IconButtonProps> = Template.bind(undefined);
Disabled.args = {
disabled: true,
icon: <InformationIcon />,
};
export const ExtraSmallSizeButton: StoryFn<IconButtonProps> =
Template.bind(undefined);
ExtraSmallSizeButton.args = {
size: 'extraSmall',
icon: <InformationIcon />,
};
export const SmallSizeButton: StoryFn<IconButtonProps> =
Template.bind(undefined);
SmallSizeButton.args = {
size: 'small',
icon: <InformationIcon />,
};
export const LargeSizeButton: StoryFn<IconButtonProps> =
Template.bind(undefined);
LargeSizeButton.args = {
size: 'large',
icon: <InformationIcon />,
};

View File

@@ -0,0 +1,65 @@
import type { Meta, StoryFn } from '@storybook/react';
import { useState } from 'react';
import { Checkbox } from './checkbox';
export default {
title: 'UI/Checkbox',
component: Checkbox,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta<typeof Checkbox>;
export const Basic: StoryFn<typeof Checkbox> = props => {
const [checked, setChecked] = useState(props.checked);
const handleChange = (
_event: React.ChangeEvent<HTMLInputElement>,
checked: boolean
) => {
setChecked(checked);
props.onChange?.(_event, checked);
};
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
justifyContent: 'center',
}}
>
<Checkbox
style={{ fontSize: 14 }}
{...props}
checked={checked}
onChange={handleChange}
/>
<Checkbox
style={{ fontSize: 16 }}
{...props}
checked={checked}
onChange={handleChange}
/>
<Checkbox
style={{ fontSize: 18 }}
{...props}
checked={checked}
onChange={handleChange}
/>
<Checkbox
style={{ fontSize: 24 }}
{...props}
checked={checked}
onChange={handleChange}
/>
</div>
);
};
Basic.args = {
checked: true,
disabled: false,
indeterminate: false,
onChange: console.log,
};

View File

@@ -0,0 +1,25 @@
import type { Meta, StoryFn } from '@storybook/react';
import { Divider, type DividerProps } from '.';
export default {
title: 'UI/Divider',
component: Divider,
} satisfies Meta<typeof Divider>;
const Template: StoryFn<DividerProps> = args => (
<div
style={{
height: '100px',
padding: '0 20px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Divider {...args} />
</div>
);
export const Default: StoryFn<DividerProps> = Template.bind(undefined);
Default.args = {};

View File

@@ -11,19 +11,18 @@ export type DividerProps = PropsWithChildren &
dividerColor?: string;
};
const defaultProps = {
orientation: 'horizontal',
size: 'default',
};
export const Divider = forwardRef<HTMLDivElement, DividerProps>(
(props, ref) => {
const { orientation, className, size, dividerColor, style, ...otherProps } =
{
...defaultProps,
...props,
};
(
{
orientation = 'horizontal',
size = 'default',
dividerColor = 'var(--affine-border-color)',
style,
className,
...otherProps
},
ref
) => {
return (
<div
ref={ref}

View File

@@ -0,0 +1,16 @@
import type { Meta, StoryFn } from '@storybook/react';
import { Empty, type EmptyContentProps } from '.';
export default {
title: 'UI/Empty',
component: Empty,
} satisfies Meta<typeof Empty>;
const Template: StoryFn<EmptyContentProps> = args => <Empty {...args} />;
export const Default: StoryFn<EmptyContentProps> = Template.bind(undefined);
Default.args = {
title: 'No Data',
description: 'No Data',
};

View File

@@ -0,0 +1,58 @@
import { InformationIcon } from '@blocksuite/icons';
import type { Meta, StoryFn } from '@storybook/react';
import { Input, type InputProps } from '.';
export default {
title: 'UI/Input',
component: Input,
} satisfies Meta<typeof Input>;
const Template: StoryFn<InputProps> = args => (
<div style={{ width: '50%' }}>
<Input {...args} />
</div>
);
export const Default: StoryFn<InputProps> = Template.bind(undefined);
Default.args = {
defaultValue: 'This is a default input',
};
export const WithPrefix: StoryFn<InputProps> = Template.bind(undefined);
WithPrefix.args = {
defaultValue: 'This is a input with prefix',
preFix: <InformationIcon />,
};
export const Large: StoryFn<InputProps> = Template.bind(undefined);
Large.args = {
placeholder: 'This is a large input',
size: 'large',
};
export const ExtraLarge: StoryFn<InputProps> = Template.bind(undefined);
ExtraLarge.args = {
placeholder: 'This is a extraLarge input',
size: 'extraLarge',
};
export const CustomWidth: StoryFn<InputProps> = Template.bind(undefined);
CustomWidth.args = {
width: 300,
placeholder: 'This is a custom width input, default is 100%',
};
export const ErrorStatus: StoryFn<InputProps> = Template.bind(undefined);
ErrorStatus.args = {
status: 'error',
placeholder: 'This is a error status input',
};
export const WarningStatus: StoryFn<InputProps> = Template.bind(undefined);
WarningStatus.args = {
status: 'warning',
placeholder: 'This is a warning status input',
};
export const Disabled: StoryFn<InputProps> = Template.bind(undefined);
Disabled.args = {
disabled: true,
placeholder: 'This is a disabled input',
};

View File

@@ -8,17 +8,17 @@ export const inputWrapper = style({
},
width: widthVar,
height: 28,
padding: '4px 10px',
color: 'var(--affine-icon-color)',
border: '1px solid var(--affine-border-color)',
backgroundColor: 'var(--affine-white-10)',
lineHeight: '22px',
padding: '0 10px',
color: 'var(--affine-text-primary-color)',
border: '1px solid',
backgroundColor: 'var(--affine-white)',
borderRadius: 8,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: 8,
// icon size
fontSize: '16px',
fontSize: 'var(--affine-font-base)',
boxSizing: 'border-box',
selectors: {
'&.no-border': {
@@ -27,14 +27,10 @@ export const inputWrapper = style({
// size
'&.large': {
height: 32,
// icon size
fontSize: '20px',
},
'&.extra-large': {
height: 40,
padding: '8px 10px',
// icon size
fontSize: '20px',
fontWeight: 600,
},
// color
'&.disabled': {
@@ -49,45 +45,34 @@ export const inputWrapper = style({
'&.warning': {
borderColor: 'var(--affine-warning-color)',
},
'&.default': {
borderColor: 'var(--affine-border-color)',
},
'&.default.focus': {
borderColor: 'var(--affine-primary-color)',
boxShadow: 'var(--affine-active-shadow)',
boxShadow: '0px 0px 0px 2px rgba(30, 150, 235, 0.30);',
},
},
});
export const input = style({
height: '100%',
width: '0',
flex: 1,
fontSize: 'var(--affine-font-xs)',
lineHeight: '20px',
fontWeight: '500',
color: 'var(--affine-text-primary-color)',
boxSizing: 'border-box',
// prevent default style
WebkitAppearance: 'none',
WebkitTapHighlightColor: 'transparent',
outline: 'none',
border: 'none',
background: 'transparent',
selectors: {
'&::placeholder': {
color: 'var(--affine-placeholder-color)',
},
'&:autofill, &:-webkit-autofill, &:-internal-autofill-selected, &:-webkit-autofill:hover, &:-webkit-autofill:focus, &:-webkit-autofill:active':
{
// The reason for using !important here is:
// The user agent style sheets of many browsers utilise !important in their :-webkit-autofill style declarations.
// https://developer.mozilla.org/en-US/docs/Web/CSS/:autofill#:~:text=%2C%20254)-,!important,-%3B%0Abackground%2Dimage
backgroundColor: 'var(--affine-white-10) !important',
['-webkit-box-shadow' as string]: 'none !important',
},
'&:disabled': {
color: 'var(--affine-text-disable-color)',
},
'&.large, &.extra-large': {
fontSize: 'var(--affine-font-base)',
lineHeight: '24px',
},
},
});

View File

@@ -0,0 +1,13 @@
import type { Meta, StoryFn } from '@storybook/react';
import { Loading, type LoadingProps } from '.';
export default {
title: 'UI/Loading',
component: Loading,
} satisfies Meta<typeof Loading>;
const Template: StoryFn<LoadingProps> = args => <Loading {...args} />;
export const Default: StoryFn<LoadingProps> = Template.bind(undefined);
Default.args = {};

View File

@@ -0,0 +1,18 @@
import type { Meta, StoryFn } from '@storybook/react';
import {
AnimatedCollectionsIcon,
type CollectionsIconProps,
} from './collections-icon';
export default {
title: 'UI/Lottie/Collection Icons',
component: AnimatedCollectionsIcon,
} satisfies Meta<typeof AnimatedCollectionsIcon>;
const Template: StoryFn<CollectionsIconProps> = args => (
<AnimatedCollectionsIcon {...args} />
);
export const Default: StoryFn<CollectionsIconProps> = Template.bind(undefined);
Default.args = {};

View File

@@ -0,0 +1,15 @@
import type { Meta, StoryFn } from '@storybook/react';
import { AnimatedDeleteIcon, type DeleteIconProps } from './delete-icon';
export default {
title: 'UI/Lottie/Delete Icon',
component: AnimatedDeleteIcon,
} satisfies Meta<typeof AnimatedDeleteIcon>;
const Template: StoryFn<DeleteIconProps> = args => (
<AnimatedDeleteIcon {...args} />
);
export const Default: StoryFn<DeleteIconProps> = Template.bind(undefined);
Default.args = {};

View File

@@ -0,0 +1,17 @@
import type { Meta, StoryFn } from '@storybook/react';
import { MenuTrigger, type MenuTriggerProps } from '.';
export default {
title: 'UI/MenuTrigger',
component: MenuTrigger,
} satisfies Meta<typeof MenuTrigger>;
const Template: StoryFn<MenuTriggerProps> = args => (
<div style={{ width: '50%' }}>
<MenuTrigger {...args}>This is a menu trigger</MenuTrigger>
</div>
);
export const Default: StoryFn<MenuTriggerProps> = Template.bind(undefined);
Default.args = {};

View File

@@ -0,0 +1,203 @@
import { InformationIcon } from '@blocksuite/icons';
import type { Meta, StoryFn } from '@storybook/react';
import { type ReactNode, useCallback, useState } from 'react';
import { Button } from '../button';
import { Tooltip } from '../tooltip';
import {
Menu,
MenuIcon,
MenuItem,
type MenuItemProps,
type MenuProps,
MenuSeparator,
MenuSub,
MenuTrigger,
} from '.';
export default {
title: 'UI/Menu',
component: Menu,
} satisfies Meta<typeof Menu>;
const Template: StoryFn<MenuProps> = args => (
<Menu
{...args}
contentOptions={{
style: {
width: '500px',
},
}}
>
<MenuTrigger>menu trigger</MenuTrigger>
</Menu>
);
interface Items {
label: ReactNode;
type?: MenuItemProps['type'];
preFix?: MenuItemProps['preFix'];
disabled?: boolean;
divider?: boolean;
subItems?: Items[];
block?: boolean;
}
const items: Items[] = [
{
label: 'default menu item 1',
},
{
label: 'menu item with icon',
preFix: (
<Tooltip content="Use `MenuIcon` to wrap your icon and choose `preFix` or `endFix`">
<MenuIcon>
<InformationIcon />
</MenuIcon>
</Tooltip>
),
},
{
label: (
<Tooltip
align="start"
content="Write, Draw, and Plan All at Once Notion Open Source Alternative One
hyper-fused platform for wildly creative minds"
>
<span>
Write, Draw, and Plan All at Once Notion Open Source Alternative One
hyper-fused platform for wildly creative minds
</span>
</Tooltip>
),
block: true,
},
{
label: 'default disabled menu item',
disabled: true,
},
{
label: 'danger menu item',
type: 'danger',
block: true,
preFix: (
<Tooltip content="Use `MenuIcon` to wrap your icon and choose `preFix` or `endFix`">
<MenuIcon>
<InformationIcon />
</MenuIcon>
</Tooltip>
),
},
{
label: 'warning menu item',
type: 'warning',
divider: true,
},
{
label: 'menu item with sub menu',
subItems: [
{
label: 'sub menu item 1',
},
{
label: 'sub menu item 1',
},
],
},
{
label: 'menu item with deep sub menu',
subItems: [
{
label: 'sub menu item 1',
},
{
label: 'sub menu with sub',
subItems: [
{
label: 'sub menu item 2-1',
},
{
label: 'sub menu item 2-2',
},
],
},
],
},
];
export const Default: StoryFn<MenuProps> = Template.bind(undefined);
const ItemRender = ({ label, divider, subItems, ...otherProps }: Items) => {
const onSelect = useCallback(() => {
console.log('value', label);
}, [label]);
if (subItems) {
return (
<>
<MenuSub
items={subItems.map((props, i) => (
<ItemRender key={i} {...props} />
))}
triggerOptions={otherProps}
>
{label}
</MenuSub>
{divider ? <MenuSeparator /> : null}
</>
);
}
return (
<>
<MenuItem onSelect={onSelect} {...otherProps}>
{label}
</MenuItem>
{divider ? <MenuSeparator /> : null}
</>
);
};
Default.args = {
items: items.map((props, i) => {
return <ItemRender key={i} {...props} />;
}),
};
const selectList = [
{ name: 'AFFiNE', value: '1' },
{ name: 'blocksuite', value: '2' },
{ name: 'octobase', value: '3' },
{ name: 'virgo', value: '4' },
];
const SelectItems = ({
selectedValue,
onSelect,
}: {
selectedValue: string;
onSelect: (value: string) => void;
}) => {
return selectList.map(({ name, value }) => (
<MenuItem
key={value}
selected={selectedValue === value}
onSelect={() => onSelect(value)}
>
{name}
</MenuItem>
));
};
const AsSelectTemplate: StoryFn<MenuProps> = () => {
const [value, setValue] = useState('1');
const name = selectList.find(item => item.value === value)?.name;
return (
<Menu items={<SelectItems selectedValue={value} onSelect={setValue} />}>
<Button>selected: {name}</Button>
</Menu>
);
};
export const AsSelect: StoryFn<MenuProps> = AsSelectTemplate.bind({});

View File

@@ -0,0 +1,69 @@
import type { Meta, StoryFn } from '@storybook/react';
import { useCallback, useState } from 'react';
import { Button } from '../button';
import { Input, type InputProps } from '../input';
import { ConfirmModal, type ConfirmModalProps } from './confirm-modal';
import { Modal, type ModalProps } from './modal';
export default {
title: 'UI/Modal',
component: Modal,
argTypes: {},
} satisfies Meta<ModalProps>;
const Template: StoryFn<ModalProps> = args => {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<Modal open={open} onOpenChange={setOpen} {...args} />
</>
);
};
export const Default: StoryFn<ModalProps> = Template.bind(undefined);
Default.args = {
title: 'Modal Title',
description:
'If the day is done, if birds sing no more, if the wind has flagged tired, then draw the veil of darkness thick upon me, even as thou hast wrapt the earth with the coverlet of sleep and tenderly closed the petals of the drooping lotus at dusk.',
};
const wait = () => new Promise(resolve => setTimeout(resolve, 1000));
const ConfirmModalTemplate: StoryFn<ConfirmModalProps> = () => {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [inputStatus, setInputStatus] =
useState<InputProps['status']>('default');
const handleConfirm = useCallback(async () => {
setLoading(true);
await wait();
setInputStatus(inputStatus !== 'error' ? 'error' : 'success');
setLoading(false);
}, [inputStatus]);
return (
<>
<Button onClick={() => setOpen(true)}>Open Confirm Modal</Button>
<ConfirmModal
open={open}
onOpenChange={setOpen}
onConfirm={handleConfirm}
title="Modal Title"
description="Modal description"
confirmButtonOptions={{
loading: loading,
type: 'primary',
children: 'Confirm',
}}
>
<Input placeholder="input someting" status={inputStatus} />
</ConfirmModal>
</>
);
};
export const Confirm: StoryFn<ModalProps> =
ConfirmModalTemplate.bind(undefined);

View File

@@ -1 +1,4 @@
/**
* @deprecated
*/
export * from './popover';

View File

@@ -0,0 +1,24 @@
import type { Meta, StoryFn } from '@storybook/react';
import { ScrollableContainer, type ScrollableContainerProps } from '.';
export default {
title: 'UI/Scrollbar',
component: ScrollableContainer,
} satisfies Meta<typeof ScrollableContainer>;
const Template: StoryFn<ScrollableContainerProps> = args => (
<div style={{ height: '100px', width: '100%' }}>
<ScrollableContainer {...args}>
<ul>
{Array.from({ length: 100 }).map((_, index) => (
<li key={index}>item {index}</li>
))}
</ul>
</ScrollableContainer>
</div>
);
export const Default: StoryFn<ScrollableContainerProps> =
Template.bind(undefined);
Default.args = {};

View File

@@ -0,0 +1,39 @@
import type { Meta, StoryFn } from '@storybook/react';
import { Skeleton, type SkeletonProps } from '.';
export default {
title: 'UI/Skeleton',
component: Skeleton,
} satisfies Meta<typeof Skeleton>;
const Template: StoryFn<SkeletonProps> = args => (
<>
{Array.from({ length: 4 }).map(i => (
<div
key={`${i}`}
style={{ width: '100%', maxWidth: '300px', marginBottom: '4px' }}
>
<Skeleton {...args} />
</div>
))}
</>
);
export const Default: StoryFn<SkeletonProps> = Template.bind(undefined);
Default.args = {};
export const Circle: StoryFn<SkeletonProps> = Template.bind(undefined);
Circle.args = {
variant: 'circular',
};
export const Rectangle: StoryFn<SkeletonProps> = Template.bind(undefined);
Rectangle.args = {
variant: 'rectangular',
};
export const Text: StoryFn<SkeletonProps> = Template.bind(undefined);
Text.args = {
variant: 'text',
};

View File

@@ -1,5 +1,8 @@
import type { HTMLAttributes, PropsWithChildren } from 'react';
/**
* @reference These props are migrated from [MUI Skeleton props](https://mui.com/material-ui/api/skeleton/#props)
*/
export interface SkeletonProps
extends PropsWithChildren,
HTMLAttributes<HTMLElement> {
@@ -12,22 +15,19 @@ export interface SkeletonProps
* The type of content that will be rendered.
* @default `'text'`
*/
variant?: 'circular' | 'rectangular' | 'rounded' | 'text' | string;
variant?: 'circular' | 'rectangular' | 'rounded' | 'text';
/**
* Width of the skeleton. Useful when the skeleton is inside an inline element with no width of its own.
* Number values are treated as pixels.
*/
width?: number | string;
/**
* Height of the skeleton. Useful when you don't want to adapt the skeleton to a text element but for instance a card.
* Number values are treated as pixels.
*/
height?: number | string;
/**
* Wrapper component. If not provided, the default element is a div.
*/
wrapper?: string;
}
export type PickStringFromUnion<T> = T extends string ? T : never;

View File

@@ -0,0 +1,13 @@
import type { Meta, StoryFn } from '@storybook/react';
import { Switch, type SwitchProps } from '.';
export default {
title: 'UI/Switch',
component: Switch,
} satisfies Meta<typeof Switch>;
const Template: StoryFn<SwitchProps> = args => <Switch {...args} />;
export const Default: StoryFn<SwitchProps> = Template.bind(undefined);
Default.args = {};

View File

@@ -9,7 +9,7 @@ import {
import * as styles from './index.css';
type SwitchProps = Omit<HTMLAttributes<HTMLLabelElement>, 'onChange'> & {
export type SwitchProps = Omit<HTMLAttributes<HTMLLabelElement>, 'onChange'> & {
checked?: boolean;
onChange?: (checked: boolean) => void;
children?: ReactNode;

View File

@@ -0,0 +1,46 @@
import type { Meta, StoryFn } from '@storybook/react';
import {
Table,
TableBody,
TableBodyRow,
TableCell,
TableHead,
TableHeadRow,
} from '.';
export default {
title: 'UI/Table',
component: Table,
} satisfies Meta<typeof Table>;
const Template: StoryFn = args => (
<Table {...args}>
<TableHead>
<TableHeadRow>
<TableCell>Title 1</TableCell>
<TableCell>Title 2</TableCell>
<TableCell>Title 3</TableCell>
<TableCell>Title 4</TableCell>
</TableHeadRow>
</TableHead>
<TableBody>
{Array.from({ length: 10 }).map((_, rowNum) => {
return (
<TableBodyRow key={`${rowNum}`}>
{Array.from({ length: 4 }).map((_, colNum) => {
return (
<TableCell key={`${rowNum}-${colNum}`}>
Cell {rowNum}-{colNum}
</TableCell>
);
})}
</TableBodyRow>
);
})}
</TableBody>
</Table>
);
export const Default: StoryFn = Template.bind(undefined);

View File

@@ -0,0 +1,20 @@
import { useCallback, useState } from 'react';
import { Button } from '../button';
import { toast } from '.';
export default {
title: 'UI/Toast',
component: () => null,
};
export const Default = () => {
const [count, setCount] = useState(1);
const showToast = useCallback(() => {
toast(`Toast ${count}`);
setCount(count + 1);
}, [count]);
return <Button onClick={showToast}>Show toast</Button>;
};

View File

@@ -0,0 +1,32 @@
import type { Meta, StoryFn } from '@storybook/react';
import { Button } from '../button';
import Tooltip, { type TooltipProps } from '.';
export default {
title: 'UI/Tooltip',
component: Tooltip,
} satisfies Meta<typeof Tooltip>;
const Template: StoryFn<TooltipProps> = args => (
<Tooltip content="This is a tooltip" {...args}>
<Button>Show tooltip</Button>
</Tooltip>
);
export const Default: StoryFn<TooltipProps> = Template.bind(undefined);
Default.args = {};
export const WithCustomContent: StoryFn<TooltipProps> = args => (
<Tooltip
content={
<ul>
<li>This is a tooltip</li>
<li style={{ color: 'red' }}>With custom content</li>
</ul>
}
{...args}
>
<Button>Show tooltip</Button>
</Tooltip>
);