mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(component): add storybook (#5079)
This commit is contained in:
52
packages/frontend/component/src/ui/avatar/avatar.stories.tsx
Normal file
52
packages/frontend/component/src/ui/avatar/avatar.stories.tsx
Normal 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);
|
||||
},
|
||||
};
|
||||
46
packages/frontend/component/src/ui/button/button.stories.tsx
Normal file
46
packages/frontend/component/src/ui/button/button.stories.tsx
Normal 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',
|
||||
};
|
||||
@@ -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 />,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 = {};
|
||||
@@ -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}
|
||||
|
||||
16
packages/frontend/component/src/ui/empty/empty.stories.tsx
Normal file
16
packages/frontend/component/src/ui/empty/empty.stories.tsx
Normal 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',
|
||||
};
|
||||
58
packages/frontend/component/src/ui/input/input.stories.tsx
Normal file
58
packages/frontend/component/src/ui/input/input.stories.tsx
Normal 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',
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 = {};
|
||||
@@ -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 = {};
|
||||
@@ -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 = {};
|
||||
@@ -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 = {};
|
||||
203
packages/frontend/component/src/ui/menu/menu.stories.tsx
Normal file
203
packages/frontend/component/src/ui/menu/menu.stories.tsx
Normal 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({});
|
||||
69
packages/frontend/component/src/ui/modal/modal.stories.tsx
Normal file
69
packages/frontend/component/src/ui/modal/modal.stories.tsx
Normal 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);
|
||||
@@ -1 +1,4 @@
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export * from './popover';
|
||||
|
||||
@@ -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 = {};
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
13
packages/frontend/component/src/ui/switch/switch.stories.tsx
Normal file
13
packages/frontend/component/src/ui/switch/switch.stories.tsx
Normal 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 = {};
|
||||
@@ -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;
|
||||
|
||||
46
packages/frontend/component/src/ui/table/table.stories.tsx
Normal file
46
packages/frontend/component/src/ui/table/table.stories.tsx
Normal 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);
|
||||
20
packages/frontend/component/src/ui/toast/toast.stories.tsx
Normal file
20
packages/frontend/component/src/ui/toast/toast.stories.tsx
Normal 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>;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
Reference in New Issue
Block a user