mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
refactor(storybook): move to apps folder (#2901)
This commit is contained in:
61
apps/storybook/.storybook/main.ts
Normal file
61
apps/storybook/.storybook/main.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { runCli } from '@magic-works/i18n-codegen';
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { mergeConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||
|
||||
runCli(
|
||||
{
|
||||
config: fileURLToPath(
|
||||
new URL('../../../.i18n-codegen.json', import.meta.url)
|
||||
),
|
||||
watch: false,
|
||||
},
|
||||
error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
|
||||
export default {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
staticDirs: ['../../../apps/web/public'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-storysource',
|
||||
'storybook-dark-mode',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
},
|
||||
async viteFinal(config, { configType }) {
|
||||
return mergeConfig(config, {
|
||||
assetsInclude: ['**/*.md'],
|
||||
plugins: [
|
||||
vanillaExtractPlugin(),
|
||||
tsconfigPaths({
|
||||
root: fileURLToPath(new URL('../../../', import.meta.url)),
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
'process.env': {},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'dotenv/config': fileURLToPath(
|
||||
new URL('../../../scripts/vitest/dotenv-config.ts', import.meta.url)
|
||||
),
|
||||
'next/config': fileURLToPath(
|
||||
new URL(
|
||||
'../../../scripts/vitest/next-config-mock.ts',
|
||||
import.meta.url
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
} as StorybookConfig;
|
||||
3
apps/storybook/.storybook/preview-head.html
Normal file
3
apps/storybook/.storybook/preview-head.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
70
apps/storybook/.storybook/preview.tsx
Normal file
70
apps/storybook/.storybook/preview.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
import { LOCALES, createI18n } from '@affine/i18n';
|
||||
import { ThemeProvider, useTheme } from 'next-themes';
|
||||
import { setupGlobal } from '@affine/env/global';
|
||||
import type { ComponentType } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useDarkMode } from 'storybook-dark-mode';
|
||||
|
||||
setupGlobal();
|
||||
|
||||
export const parameters = {
|
||||
backgrounds: { disable: true },
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const globalTypes = {
|
||||
locale: {
|
||||
name: 'Locale',
|
||||
description: 'Internationalization locale',
|
||||
defaultValue: 'en',
|
||||
toolbar: {
|
||||
icon: 'globe',
|
||||
items: LOCALES.map(locale => ({
|
||||
title: locale.originalName,
|
||||
value: locale.tag,
|
||||
right: locale.flagEmoji,
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const createI18nDecorator = () => {
|
||||
const i18n = createI18n();
|
||||
const withI18n = (Story: any, context: any) => {
|
||||
const locale = context.globals.locale;
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(locale);
|
||||
}, [locale]);
|
||||
return <Story {...context} />;
|
||||
};
|
||||
return withI18n;
|
||||
};
|
||||
|
||||
const Component = () => {
|
||||
const isDark = useDarkMode();
|
||||
const theme = useTheme();
|
||||
useEffect(() => {
|
||||
theme.setTheme(isDark ? 'dark' : 'light');
|
||||
}, [isDark]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story: ComponentType) => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Component />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
);
|
||||
},
|
||||
createI18nDecorator(),
|
||||
];
|
||||
52
apps/storybook/package.json
Normal file
52
apps/storybook/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@affine/storybook",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "storybook dev -p 6006",
|
||||
"build": "NODE_OPTIONS=--max_old_space_size=4096 storybook build",
|
||||
"test": "test-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@storybook/addon-actions": "^7.0.23",
|
||||
"@storybook/addon-essentials": "^7.0.23",
|
||||
"@storybook/addon-interactions": "^7.0.23",
|
||||
"@storybook/addon-links": "^7.0.23",
|
||||
"@storybook/addon-storysource": "^7.0.23",
|
||||
"@storybook/blocks": "^7.0.23",
|
||||
"@storybook/builder-vite": "^7.0.23",
|
||||
"@storybook/jest": "^0.1.0",
|
||||
"@storybook/react": "^7.0.23",
|
||||
"@storybook/react-vite": "^7.0.23",
|
||||
"@storybook/test-runner": "^0.11.0",
|
||||
"@storybook/testing-library": "^0.2.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"concurrently": "^8.2.0",
|
||||
"jest-mock": "^29.5.0",
|
||||
"serve": "^14.2.0",
|
||||
"storybook": "^7.0.21",
|
||||
"storybook-dark-mode": "^3.0.0",
|
||||
"wait-on": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/block-std": "0.0.0-20230627165830-836e6fd1-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230627165830-836e6fd1-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230627165830-836e6fd1-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230627165830-836e6fd1-nightly",
|
||||
"@blocksuite/icons": "^2.1.21",
|
||||
"@blocksuite/lit": "0.0.0-20230627165830-836e6fd1-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230627165830-836e6fd1-nightly",
|
||||
"react": "18.3.0-canary-8ec962d82-20230623",
|
||||
"react-dom": "18.3.0-canary-8ec962d82-20230623"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@blocksuite/blocks": "*",
|
||||
"@blocksuite/editor": "*",
|
||||
"@blocksuite/global": "*",
|
||||
"@blocksuite/icons": "*",
|
||||
"@blocksuite/lit": "*",
|
||||
"@blocksuite/store": "*"
|
||||
},
|
||||
"version": "0.7.0-canary.20"
|
||||
}
|
||||
36
apps/storybook/src/stories/affine-banner.stories.tsx
Normal file
36
apps/storybook/src/stories/affine-banner.stories.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { BrowserWarning, DownloadTips } from '@affine/component/affine-banner';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Banner',
|
||||
component: BrowserWarning,
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
const [closed, setIsClosed] = useState(true);
|
||||
return (
|
||||
<div>
|
||||
<BrowserWarning
|
||||
message={<span>test</span>}
|
||||
show={closed}
|
||||
onClose={() => {
|
||||
setIsClosed(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Download: StoryFn = () => {
|
||||
const [, setIsClosed] = useState(true);
|
||||
return (
|
||||
<div>
|
||||
<DownloadTips
|
||||
onClose={() => {
|
||||
setIsClosed(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
24
apps/storybook/src/stories/affine-loading.stories.tsx
Normal file
24
apps/storybook/src/stories/affine-loading.stories.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { AffineLoading } from '@affine/component/affine-loading';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Loading',
|
||||
component: AffineLoading,
|
||||
};
|
||||
|
||||
export const Default: StoryFn = ({ width, loop, autoplay, autoReverse }) => (
|
||||
<div
|
||||
style={{
|
||||
width: width,
|
||||
height: width,
|
||||
}}
|
||||
>
|
||||
<AffineLoading loop={loop} autoplay={autoplay} autoReverse={autoReverse} />
|
||||
</div>
|
||||
);
|
||||
Default.args = {
|
||||
width: 100,
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
autoReverse: true,
|
||||
};
|
||||
162
apps/storybook/src/stories/app-sidebar.stories.tsx
Normal file
162
apps/storybook/src/stories/app-sidebar.stories.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import {
|
||||
AppSidebar,
|
||||
AppSidebarFallback,
|
||||
appSidebarOpenAtom,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { AddPageButton } from '@affine/component/app-sidebar';
|
||||
import { CategoryDivider } from '@affine/component/app-sidebar';
|
||||
import {
|
||||
navHeaderStyle,
|
||||
sidebarButtonStyle,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
||||
import { QuickSearchInput } from '@affine/component/app-sidebar';
|
||||
import {
|
||||
SidebarContainer,
|
||||
SidebarScrollableContainer,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
SettingsIcon,
|
||||
SidebarIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { type PropsWithChildren, useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'Components/AppSidebar',
|
||||
component: AppSidebar,
|
||||
} satisfies Meta;
|
||||
|
||||
const Container = ({ children }: PropsWithChildren) => (
|
||||
<main
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: 'calc(100vh - 40px)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
const Main = () => {
|
||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||
return (
|
||||
<div>
|
||||
<div className={navHeaderStyle}>
|
||||
{!open && (
|
||||
<IconButton
|
||||
className={sidebarButtonStyle}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<SidebarIcon width={24} height={24} />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<AppSidebar />
|
||||
<Main />
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Fallback = () => {
|
||||
return (
|
||||
<Container>
|
||||
<AppSidebarFallback />
|
||||
<Main />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithItems: StoryFn = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
return (
|
||||
<Container>
|
||||
<AppSidebar>
|
||||
<SidebarContainer>
|
||||
<QuickSearchInput />
|
||||
<div style={{ height: '20px' }} />
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
</SidebarContainer>
|
||||
|
||||
<SidebarScrollableContainer>
|
||||
<CategoryDivider label="Favorites" />
|
||||
<MenuLinkItem
|
||||
collapsed={collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Collapsible Item
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
collapsed={!collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Collapsible Item
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
|
||||
<CategoryDivider label="Others" />
|
||||
<MenuLinkItem
|
||||
icon={<DeleteTemporarilyIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Trash
|
||||
</MenuLinkItem>
|
||||
</SidebarScrollableContainer>
|
||||
<SidebarContainer>
|
||||
<AddPageButton />
|
||||
</SidebarContainer>
|
||||
</AppSidebar>
|
||||
<Main />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
86
apps/storybook/src/stories/block-suite-editor.stories.tsx
Normal file
86
apps/storybook/src/stories/block-suite-editor.stories.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import type { EditorProps } from '@affine/component/block-suite-editor';
|
||||
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { createMemoryStorage, Workspace } from '@blocksuite/store';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { use } from 'react';
|
||||
|
||||
const blockSuiteWorkspace = new Workspace({
|
||||
id: 'test',
|
||||
blobStorages: [createMemoryStorage],
|
||||
});
|
||||
|
||||
async function initPage(page: Page) {
|
||||
await page.waitForLoaded();
|
||||
// Add page block and surface block at root level
|
||||
const pageBlockId = page.addBlock('affine:page', {
|
||||
title: new page.Text('Hello, world!'),
|
||||
});
|
||||
page.addBlock('affine:surface', {}, pageBlockId);
|
||||
const frameId = page.addBlock('affine:note', {}, pageBlockId);
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('This is a paragraph.'),
|
||||
},
|
||||
frameId
|
||||
);
|
||||
page.resetHistory();
|
||||
}
|
||||
|
||||
blockSuiteWorkspace.register(AffineSchemas).register(__unstableSchemas);
|
||||
const page = blockSuiteWorkspace.createPage('page0');
|
||||
|
||||
type BlockSuiteMeta = Meta<typeof BlockSuiteEditor>;
|
||||
export default {
|
||||
title: 'BlockSuite/Editor',
|
||||
component: BlockSuiteEditor,
|
||||
} satisfies BlockSuiteMeta;
|
||||
|
||||
const Template: StoryFn<EditorProps> = (props: Partial<EditorProps>) => {
|
||||
if (!page.loaded) {
|
||||
use(initPage(page));
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<BlockSuiteEditor onInit={initPage} page={page} mode="page" {...props} />
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
}}
|
||||
id="toolWrapper"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.play = async ({ canvasElement }) => {
|
||||
await new Promise<void>(resolve => {
|
||||
setTimeout(() => resolve(), 500);
|
||||
});
|
||||
const editorContainer = canvasElement.querySelector(
|
||||
'[data-testid="editor-page0"]'
|
||||
) as HTMLDivElement;
|
||||
expect(editorContainer).not.toBeNull();
|
||||
const editor = editorContainer.querySelector(
|
||||
'editor-container'
|
||||
) as EditorContainer;
|
||||
expect(editor).not.toBeNull();
|
||||
};
|
||||
|
||||
Empty.args = {
|
||||
mode: 'page',
|
||||
};
|
||||
38
apps/storybook/src/stories/breadcrumbs.stories.tsx
Normal file
38
apps/storybook/src/stories/breadcrumbs.stories.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { Breadcrumbs } from '@affine/component';
|
||||
import { Link, Typography } from '@mui/material';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { within } from '@storybook/testing-library';
|
||||
export default {
|
||||
title: 'AFFiNE/Breadcrumbs',
|
||||
component: Breadcrumbs,
|
||||
} as Meta<typeof Breadcrumbs>;
|
||||
|
||||
const Template: StoryFn = args => <Breadcrumbs {...args} />;
|
||||
|
||||
export const Primary = Template.bind(undefined);
|
||||
Primary.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const text = canvas.getByText('AFFiNE');
|
||||
expect(text.getAttribute('data-testid')).toBe('affine');
|
||||
};
|
||||
Primary.args = {
|
||||
children: [
|
||||
<Link
|
||||
data-testid="affine"
|
||||
key="1"
|
||||
underline="hover"
|
||||
color="inherit"
|
||||
href="/"
|
||||
>
|
||||
AFFiNE
|
||||
</Link>,
|
||||
<Link key="2" underline="hover" color="inherit" href="/Docs/">
|
||||
Docs
|
||||
</Link>,
|
||||
<Typography key="3" color="text.primary">
|
||||
Introduction
|
||||
</Typography>,
|
||||
],
|
||||
};
|
||||
137
apps/storybook/src/stories/button.stories.tsx
Normal file
137
apps/storybook/src/stories/button.stories.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import type { ButtonProps } from '@affine/component';
|
||||
import { Button } from '@affine/component';
|
||||
import { DropdownButton } from '@affine/component';
|
||||
import { RadioButton, RadioButtonGroup } from '@affine/component';
|
||||
import { Menu } from '@affine/component';
|
||||
import { toast } from '@affine/component';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Button',
|
||||
component: Button,
|
||||
argTypes: {
|
||||
hoverBackground: { control: 'color' },
|
||||
hoverColor: { control: 'color' },
|
||||
},
|
||||
} as Meta<ButtonProps>;
|
||||
|
||||
const Template: StoryFn<ButtonProps> = args => <Button {...args} />;
|
||||
|
||||
export const Primary = Template.bind(undefined);
|
||||
Primary.args = {
|
||||
type: 'primary',
|
||||
children: 'This is a primary button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Default = Template.bind(undefined);
|
||||
Default.args = {
|
||||
type: 'default',
|
||||
children: 'This is a default button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Light = Template.bind(undefined);
|
||||
Light.args = {
|
||||
type: 'light',
|
||||
children: 'This is a light button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Warning = Template.bind(undefined);
|
||||
Warning.args = {
|
||||
type: 'warning',
|
||||
children: 'This is a warning button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Danger = Template.bind(undefined);
|
||||
Danger.args = {
|
||||
type: 'danger',
|
||||
children: 'This is a danger button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Dropdown: StoryFn = ({ onClickDropDown, ...props }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<DropdownButton onClickDropDown={onClickDropDown} {...props}>
|
||||
Dropdown Button
|
||||
</DropdownButton>
|
||||
|
||||
<Menu
|
||||
visible={open}
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
width={235}
|
||||
disablePortal={true}
|
||||
onClickAway={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
content={<>Dropdown Menu</>}
|
||||
>
|
||||
<DropdownButton
|
||||
onClick={() => {
|
||||
toast('Click button');
|
||||
setOpen(false);
|
||||
}}
|
||||
onClickDropDown={() => setOpen(!open)}
|
||||
>
|
||||
Dropdown with Menu
|
||||
</DropdownButton>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Dropdown.args = {
|
||||
onClick: () => toast('Click button'),
|
||||
onClickDropDown: () => toast('Click dropdown'),
|
||||
};
|
||||
|
||||
export const RadioGroup: StoryFn = ({ ...props }) => {
|
||||
return (
|
||||
<>
|
||||
<RadioButtonGroup {...props}>
|
||||
<RadioButton value="all">All</RadioButton>
|
||||
<RadioButton value="page">Page</RadioButton>
|
||||
<RadioButton value="edgeless">Edgeless</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
<RadioButtonGroup {...props}>
|
||||
<RadioButton value="all">Long long text and some more</RadioButton>
|
||||
<RadioButton value="page">Lorem ipsum dolor sit amet</RadioButton>
|
||||
<RadioButton value="edgeless">Long long text</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
RadioGroup.args = {
|
||||
defaultValue: 'all',
|
||||
onValueChange: (value: string) => toast(`Radio value: ${value}`),
|
||||
};
|
||||
|
||||
export const Test: StoryFn<ButtonProps> = () => {
|
||||
const [input, setInput] = useState('');
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
data-testid="test-input"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setInput('');
|
||||
}}
|
||||
data-testid="clear-button"
|
||||
>
|
||||
clear
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Test.storyName = 'Click Test';
|
||||
53
apps/storybook/src/stories/card.stories.tsx
Normal file
53
apps/storybook/src/stories/card.stories.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { BlockCard } from '@affine/component/card/block-card';
|
||||
import { WorkspaceCard } from '@affine/component/card/workspace-card';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Card',
|
||||
component: WorkspaceCard,
|
||||
};
|
||||
|
||||
const blockSuiteWorkspace = new Workspace({
|
||||
id: 'blocksuite-local',
|
||||
});
|
||||
|
||||
blockSuiteWorkspace.meta.setName('Hello World');
|
||||
|
||||
export const AffineWorkspaceCard = () => {
|
||||
return (
|
||||
<WorkspaceCard
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace,
|
||||
}}
|
||||
onClick={() => {}}
|
||||
onSettingClick={() => {}}
|
||||
currentWorkspaceId={null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AffineBlockCard = () => {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<BlockCard title={'New Page'} onClick={() => toast('clicked')} />
|
||||
<BlockCard
|
||||
title={'New Page'}
|
||||
desc={'Write with a blank page'}
|
||||
right={<PageIcon width={20} height={20} />}
|
||||
onClick={() => toast('clicked page')}
|
||||
/>
|
||||
<BlockCard
|
||||
title={'New Edgeless'}
|
||||
desc={'Draw with a blank whiteboard'}
|
||||
left={<PageIcon width={20} height={20} />}
|
||||
right={<EdgelessIcon width={20} height={20} />}
|
||||
onClick={() => toast('clicked edgeless')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
47
apps/storybook/src/stories/change-log.stories.tsx
Normal file
47
apps/storybook/src/stories/change-log.stories.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ChangeLog } from '@affine/component/changeLog';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { within } from '@storybook/testing-library';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/ChangeLog',
|
||||
component: ChangeLog,
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => (
|
||||
<div
|
||||
style={{
|
||||
width: '256px',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<ChangeLog onCloseWhatsNew={() => {}} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Close: StoryFn = () => {
|
||||
const [closed, setIsClosed] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div>Closed: {closed ? 'true' : 'false'}</div>
|
||||
<div
|
||||
style={{
|
||||
width: '256px',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<ChangeLog
|
||||
onCloseWhatsNew={() => {
|
||||
setIsClosed(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Close.play = async ({ canvasElement }) => {
|
||||
const element = within(canvasElement);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
element.getByTestId('change-log-close-button').click();
|
||||
};
|
||||
35
apps/storybook/src/stories/contact-modal.stories.tsx
Normal file
35
apps/storybook/src/stories/contact-modal.stories.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Button } from '@affine/component';
|
||||
import type { ContactModalProps } from '@affine/component/contact-modal';
|
||||
import { ContactModal } from '@affine/component/contact-modal';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/ContactModal',
|
||||
component: ContactModal,
|
||||
};
|
||||
|
||||
export const Basic: StoryFn<ContactModalProps> = args => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
<ContactModal
|
||||
{...args}
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Basic.args = {
|
||||
logoSrc: '/imgs/affine-text-logo.png',
|
||||
};
|
||||
13
apps/storybook/src/stories/datepicker.stories.tsx
Normal file
13
apps/storybook/src/stories/datepicker.stories.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { AFFiNEDatePicker } from '@affine/component/date-picker';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/AFFiNEDatePicker',
|
||||
component: AFFiNEDatePicker,
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
const [value, setValue] = useState<string>(new Date().toString());
|
||||
return <AFFiNEDatePicker value={value} onChange={setValue} />;
|
||||
};
|
||||
67
apps/storybook/src/stories/image-preview-modal.stories.tsx
Normal file
67
apps/storybook/src/stories/image-preview-modal.stories.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
|
||||
import { ImagePreviewModal } from '@affine/component/image-preview-modal';
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'Component/ImagePreviewModal',
|
||||
component: ImagePreviewModal,
|
||||
} satisfies Meta;
|
||||
|
||||
const workspace = createEmptyBlockSuiteWorkspace(
|
||||
'test',
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
const page = workspace.createPage('page0');
|
||||
initEmptyPage(page);
|
||||
fetch(new URL('@affine-test/fixtures/large-image.png', import.meta.url))
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(async buffer => {
|
||||
const id = await workspace.blobs.set(
|
||||
new Blob([buffer], { type: 'image/png' })
|
||||
);
|
||||
const frameId = page.getBlockByFlavour('affine:note')[0].id;
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('Please double click the image to preview it.'),
|
||||
},
|
||||
frameId
|
||||
);
|
||||
page.addBlock(
|
||||
'affine:image',
|
||||
{
|
||||
sourceId: id,
|
||||
},
|
||||
frameId
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load large-image.png', err);
|
||||
});
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<BlockSuiteEditor mode="page" page={page} onInit={initEmptyPage} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
}}
|
||||
id="toolWrapper"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
18
apps/storybook/src/stories/introduction.stories.mdx
Normal file
18
apps/storybook/src/stories/introduction.stories.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="Introduction" />
|
||||
|
||||
# AFFiNE UI Storybook
|
||||
|
||||
This is a UI component dev environment for AFFiNE UI.
|
||||
It allows you to browse the AFFiNE UI components,
|
||||
view the different states of each component,
|
||||
and interactively develop and test components.
|
||||
|
||||
## Bug Reporting
|
||||
|
||||
If you find a bug, please file an issue on [GitHub](https://github.com/toeverything/AFFiNE/issues)
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! [Get started here](https://github.com/toeverything/AFFiNE/blob/master/docs/BUILDING.md)
|
||||
202
apps/storybook/src/stories/notification-center.stories.tsx
Normal file
202
apps/storybook/src/stories/notification-center.stories.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
expandNotificationCenterAtom,
|
||||
NotificationCenter,
|
||||
pushNotificationAtom,
|
||||
} from '@affine/component/notification-center';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/NotificationCenter',
|
||||
component: NotificationCenter,
|
||||
} satisfies Meta<typeof NotificationCenter>;
|
||||
|
||||
let id = 0;
|
||||
export const Basic = () => {
|
||||
const push = useSetAtom(pushNotificationAtom);
|
||||
const expand = useAtomValue(expandNotificationCenterAtom);
|
||||
return (
|
||||
<>
|
||||
<div>{expand ? 'expanded' : 'collapsed'}</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: `${key} message`,
|
||||
timeout: 3000,
|
||||
progressingBar: true,
|
||||
undo: async () => {
|
||||
console.log('undo');
|
||||
},
|
||||
type: 'info',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Push timeout notification
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: `${key} message`,
|
||||
type: 'info',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Push notification with no timeout
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'info',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Push notification with no message
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'success',
|
||||
theme: 'light',
|
||||
});
|
||||
}}
|
||||
>
|
||||
light success
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'success',
|
||||
theme: 'dark',
|
||||
});
|
||||
}}
|
||||
>
|
||||
dark success
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'info',
|
||||
theme: 'light',
|
||||
});
|
||||
}}
|
||||
>
|
||||
light info
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'info',
|
||||
theme: 'dark',
|
||||
});
|
||||
}}
|
||||
>
|
||||
dark info
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'warning',
|
||||
theme: 'light',
|
||||
});
|
||||
}}
|
||||
>
|
||||
light warning
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'warning',
|
||||
theme: 'dark',
|
||||
});
|
||||
}}
|
||||
>
|
||||
dark warning
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'error',
|
||||
theme: 'light',
|
||||
});
|
||||
}}
|
||||
>
|
||||
light error
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'error',
|
||||
theme: 'dark',
|
||||
});
|
||||
}}
|
||||
>
|
||||
dark error
|
||||
</button>
|
||||
</div>
|
||||
<NotificationCenter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
15
apps/storybook/src/stories/onboarding-modal.stories.tsx
Normal file
15
apps/storybook/src/stories/onboarding-modal.stories.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { TourModal } from '@affine/component/tour-modal';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/TourModal',
|
||||
component: TourModal,
|
||||
};
|
||||
|
||||
export const Basic: StoryFn = () => {
|
||||
return <TourModal open={true} onClose={() => {}} />;
|
||||
};
|
||||
Basic.args = {
|
||||
logoSrc: '/imgs/affine-text-logo.png',
|
||||
};
|
||||
11
apps/storybook/src/stories/page-detail-skeleton.stories.tsx
Normal file
11
apps/storybook/src/stories/page-detail-skeleton.stories.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/PageDetailSkeleton',
|
||||
component: PageDetailSkeleton,
|
||||
} satisfies Meta<typeof PageDetailSkeleton>;
|
||||
|
||||
export const Basic = () => {
|
||||
return <PageDetailSkeleton />;
|
||||
};
|
||||
206
apps/storybook/src/stories/page-list.stories.tsx
Normal file
206
apps/storybook/src/stories/page-list.stories.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { Empty } from '@affine/component';
|
||||
import { toast } from '@affine/component';
|
||||
import { AffineLoading } from '@affine/component/affine-loading';
|
||||
import type { OperationCellProps } from '@affine/component/page-list';
|
||||
import { PageListTrashView } from '@affine/component/page-list';
|
||||
import { PageList } from '@affine/component/page-list';
|
||||
import { NewPageButton } from '@affine/component/page-list';
|
||||
import { OperationCell } from '@affine/component/page-list';
|
||||
import { PageIcon } from '@blocksuite/icons';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { userEvent } from '@storybook/testing-library';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/PageList',
|
||||
component: AffineLoading,
|
||||
};
|
||||
|
||||
export const AffineOperationCell: StoryFn<OperationCellProps> = ({
|
||||
...props
|
||||
}) => <OperationCell {...props} />;
|
||||
|
||||
AffineOperationCell.args = {
|
||||
title: 'Example Page',
|
||||
favorite: false,
|
||||
isPublic: true,
|
||||
onToggleFavoritePage: () => toast('Toggle favorite page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
onRemoveToTrash: () => toast('Remove to trash'),
|
||||
};
|
||||
AffineOperationCell.play = async ({ canvasElement }) => {
|
||||
{
|
||||
const button = canvasElement.querySelector(
|
||||
'[data-testid="page-list-operation-button"]'
|
||||
) as HTMLButtonElement;
|
||||
expect(button).not.toBeNull();
|
||||
userEvent.click(button);
|
||||
}
|
||||
};
|
||||
|
||||
export const AffineNewPageButton: StoryFn<typeof NewPageButton> = ({
|
||||
...props
|
||||
}) => <NewPageButton {...props} />;
|
||||
AffineNewPageButton.args = {
|
||||
createNewPage: () => toast('Create new page'),
|
||||
createNewEdgeless: () => toast('Create new edgeless'),
|
||||
};
|
||||
|
||||
AffineNewPageButton.play = async ({ canvasElement }) => {
|
||||
const button = canvasElement.querySelector('button') as HTMLButtonElement;
|
||||
expect(button).not.toBeNull();
|
||||
const dropdown = button.querySelector('svg') as SVGSVGElement;
|
||||
expect(dropdown).not.toBeNull();
|
||||
userEvent.click(dropdown);
|
||||
};
|
||||
|
||||
export const AffineAllPageList: StoryFn<typeof PageList> = ({ ...props }) => (
|
||||
<PageList {...props} />
|
||||
);
|
||||
|
||||
AffineAllPageList.args = {
|
||||
isPublicWorkspace: false,
|
||||
onCreateNewPage: () => toast('Create new page'),
|
||||
onCreateNewEdgeless: () => toast('Create new edgeless'),
|
||||
onImportFile: () => toast('Import file'),
|
||||
list: [
|
||||
{
|
||||
pageId: '1',
|
||||
favorite: false,
|
||||
icon: <PageIcon />,
|
||||
isPublicPage: true,
|
||||
title: 'Today Page',
|
||||
preview: 'this is page preview',
|
||||
createDate: new Date(),
|
||||
updatedDate: new Date(),
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
removeToTrash: () => toast('Remove to trash'),
|
||||
},
|
||||
{
|
||||
pageId: '3',
|
||||
favorite: false,
|
||||
icon: <PageIcon />,
|
||||
isPublicPage: true,
|
||||
title:
|
||||
'1 Example Public Page with long title that will be truncated because it is too too long',
|
||||
preview:
|
||||
'this is page preview and it is very long and will be truncated because it is too long and it is very long and will be truncated because it is too long',
|
||||
createDate: new Date('2021-01-01'),
|
||||
updatedDate: new Date('2021-01-02'),
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
removeToTrash: () => toast('Remove to trash'),
|
||||
},
|
||||
{
|
||||
pageId: '2',
|
||||
favorite: true,
|
||||
isPublicPage: false,
|
||||
icon: <PageIcon />,
|
||||
title: '2 Favorited Page 2021',
|
||||
createDate: new Date('2021-01-02'),
|
||||
updatedDate: new Date('2021-01-01'),
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
removeToTrash: () => toast('Remove to trash'),
|
||||
},
|
||||
{
|
||||
pageId: '4',
|
||||
favorite: false,
|
||||
isPublicPage: false,
|
||||
icon: <PageIcon />,
|
||||
title: 'page created in 2023-04-01',
|
||||
createDate: new Date('2023-04-01'),
|
||||
updatedDate: new Date('2023-04-01'),
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
removeToTrash: () => toast('Remove to trash'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const AffineEmptyAllPageList: StoryFn<typeof PageList> = ({
|
||||
...props
|
||||
}) => <PageList {...props} />;
|
||||
|
||||
AffineEmptyAllPageList.args = {
|
||||
isPublicWorkspace: false,
|
||||
onCreateNewPage: () => toast('Create new page'),
|
||||
onCreateNewEdgeless: () => toast('Create new edgeless'),
|
||||
onImportFile: () => toast('Import file'),
|
||||
list: [],
|
||||
fallback: (
|
||||
<Empty
|
||||
title="Empty"
|
||||
description={
|
||||
<div>
|
||||
empty description, click{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
toast('click');
|
||||
}}
|
||||
>
|
||||
button
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const AffinePublicPageList: StoryFn<typeof PageList> = ({
|
||||
...props
|
||||
}) => <PageList {...props} />;
|
||||
AffinePublicPageList.args = {
|
||||
...AffineAllPageList.args,
|
||||
isPublicWorkspace: true,
|
||||
};
|
||||
|
||||
export const AffineAllPageMobileList: StoryFn<typeof PageList> = ({
|
||||
...props
|
||||
}) => <PageList {...props} />;
|
||||
|
||||
AffineAllPageMobileList.args = AffineAllPageList.args;
|
||||
AffineAllPageMobileList.parameters = {
|
||||
viewport: {
|
||||
defaultViewport: 'mobile2',
|
||||
},
|
||||
};
|
||||
|
||||
export const AffineTrashPageList: StoryFn<typeof PageListTrashView> = ({
|
||||
...props
|
||||
}) => <PageListTrashView {...props} />;
|
||||
|
||||
AffineTrashPageList.args = {
|
||||
list: [
|
||||
{
|
||||
pageId: '1',
|
||||
icon: <PageIcon />,
|
||||
title: 'Example Page',
|
||||
preview: 'this is trash page preview',
|
||||
createDate: new Date('2021-01-01'),
|
||||
trashDate: new Date('2021-01-01'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onPermanentlyDeletePage: () => toast('Permanently delete page'),
|
||||
onRestorePage: () => toast('Restore page'),
|
||||
},
|
||||
{
|
||||
pageId: '2',
|
||||
icon: <PageIcon />,
|
||||
title: 'Example Page with long title that will be truncated',
|
||||
createDate: new Date('2021-01-01'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onPermanentlyDeletePage: () => toast('Permanently delete page'),
|
||||
onRestorePage: () => toast('Restore page'),
|
||||
},
|
||||
],
|
||||
};
|
||||
138
apps/storybook/src/stories/share-menu.stories.tsx
Normal file
138
apps/storybook/src/stories/share-menu.stories.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { toast } from '@affine/component';
|
||||
import {
|
||||
PublicLinkDisableModal,
|
||||
StyledDisableButton,
|
||||
} from '@affine/component/share-menu';
|
||||
import { ShareMenu } from '@affine/component/share-menu';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
PermissionType,
|
||||
WorkspaceType,
|
||||
} from '@affine/env/workspace/legacy-cloud';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { use, useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/ShareMenu',
|
||||
component: ShareMenu,
|
||||
};
|
||||
|
||||
async function initPage(page: Page) {
|
||||
await page.waitForLoaded();
|
||||
// Add page block and surface block at root level
|
||||
const pageBlockId = page.addBlock('affine:page', {
|
||||
title: new page.Text('Hello, world!'),
|
||||
});
|
||||
page.addBlock('affine:surface', {}, pageBlockId);
|
||||
const frameId = page.addBlock('affine:note', {}, pageBlockId);
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('This is a paragraph.'),
|
||||
},
|
||||
frameId
|
||||
);
|
||||
page.resetHistory();
|
||||
}
|
||||
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
'test-workspace',
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
|
||||
const promise = Promise.all([
|
||||
initPage(blockSuiteWorkspace.createPage({ id: 'page0' })),
|
||||
initPage(blockSuiteWorkspace.createPage({ id: 'page1' })),
|
||||
initPage(blockSuiteWorkspace.createPage({ id: 'page2' })),
|
||||
]);
|
||||
|
||||
const localWorkspace: LocalWorkspace = {
|
||||
id: 'test-workspace',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace,
|
||||
};
|
||||
|
||||
const affineWorkspace: AffineLegacyCloudWorkspace = {
|
||||
id: 'test-workspace',
|
||||
flavour: WorkspaceFlavour.AFFINE,
|
||||
blockSuiteWorkspace,
|
||||
public: false,
|
||||
type: WorkspaceType.Normal,
|
||||
permission: PermissionType.Owner,
|
||||
};
|
||||
|
||||
async function unimplemented() {
|
||||
toast('work in progress');
|
||||
}
|
||||
|
||||
export const Basic: StoryFn = () => {
|
||||
use(promise);
|
||||
return (
|
||||
<ShareMenu
|
||||
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||
workspace={localWorkspace}
|
||||
onEnableAffineCloud={unimplemented}
|
||||
onOpenWorkspaceSettings={unimplemented}
|
||||
togglePagePublic={unimplemented}
|
||||
toggleWorkspacePublish={unimplemented}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Basic.play = async ({ canvasElement }) => {
|
||||
{
|
||||
const button = canvasElement.querySelector(
|
||||
'[data-testid="share-menu-button"]'
|
||||
) as HTMLButtonElement;
|
||||
expect(button).not.toBeNull();
|
||||
button.click();
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
{
|
||||
const button = canvasElement.querySelector(
|
||||
'[data-testid="share-menu-enable-affine-cloud-button"]'
|
||||
);
|
||||
expect(button).not.toBeNull();
|
||||
}
|
||||
};
|
||||
|
||||
export const AffineBasic: StoryFn = () => {
|
||||
use(promise);
|
||||
return (
|
||||
<ShareMenu
|
||||
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||
workspace={affineWorkspace}
|
||||
onEnableAffineCloud={unimplemented}
|
||||
onOpenWorkspaceSettings={unimplemented}
|
||||
togglePagePublic={unimplemented}
|
||||
toggleWorkspacePublish={unimplemented}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DisableModal: StoryFn = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
use(promise);
|
||||
return (
|
||||
<>
|
||||
<StyledDisableButton onClick={() => setOpen(!open)}>
|
||||
Disable Public Link
|
||||
</StyledDisableButton>
|
||||
<PublicLinkDisableModal
|
||||
open={open}
|
||||
onConfirmDisable={() => {
|
||||
toast('Disabled');
|
||||
setOpen(false);
|
||||
}}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
12
apps/storybook/src/stories/switch.stories.tsx
Normal file
12
apps/storybook/src/stories/switch.stories.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { Switch } from '@affine/component';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Switch',
|
||||
component: Switch,
|
||||
};
|
||||
|
||||
export const Basic: StoryFn = () => {
|
||||
return <Switch />;
|
||||
};
|
||||
85
apps/storybook/src/stories/workspace-avatar.stories.tsx
Normal file
85
apps/storybook/src/stories/workspace-avatar.stories.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { WorkspaceAvatarProps } from '@affine/component/workspace-avatar';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/WorkspaceAvatar',
|
||||
component: WorkspaceAvatar,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: {
|
||||
type: 'range',
|
||||
min: 20,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<WorkspaceAvatarProps>;
|
||||
|
||||
const basicBlockSuiteWorkspace = new Workspace({
|
||||
id: 'blocksuite-local',
|
||||
});
|
||||
|
||||
basicBlockSuiteWorkspace.meta.setName('Hello World');
|
||||
|
||||
export const Basic: StoryFn<WorkspaceAvatarProps> = props => {
|
||||
return (
|
||||
<WorkspaceAvatar
|
||||
{...props}
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace: basicBlockSuiteWorkspace,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Basic.args = {
|
||||
size: 40,
|
||||
};
|
||||
|
||||
const avatarBlockSuiteWorkspace = new Workspace({
|
||||
id: 'blocksuite-local',
|
||||
});
|
||||
|
||||
avatarBlockSuiteWorkspace.meta.setName('Hello World');
|
||||
fetch(new URL('@affine-test/fixtures/smile.png', import.meta.url))
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(async buffer => {
|
||||
const id = await avatarBlockSuiteWorkspace.blobs.set(
|
||||
new Blob([buffer], { type: 'image/png' })
|
||||
);
|
||||
avatarBlockSuiteWorkspace.meta.setAvatar(id);
|
||||
})
|
||||
.catch(() => {
|
||||
// just ignore
|
||||
console.error('Failed to load smile.png');
|
||||
});
|
||||
|
||||
export const BlobExample: StoryFn<WorkspaceAvatarProps> = props => {
|
||||
return (
|
||||
<WorkspaceAvatar
|
||||
{...props}
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace: avatarBlockSuiteWorkspace,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
BlobExample.args = {
|
||||
size: 40,
|
||||
};
|
||||
|
||||
export const Empty: StoryFn<WorkspaceAvatarProps> = props => {
|
||||
return <WorkspaceAvatar {...props} workspace={null} />;
|
||||
};
|
||||
|
||||
Empty.args = {
|
||||
size: 40,
|
||||
};
|
||||
69
apps/storybook/src/stories/workspace-list.stories.tsx
Normal file
69
apps/storybook/src/stories/workspace-list.stories.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { WorkspaceListProps } from '@affine/component/workspace-list';
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/WorkspaceList',
|
||||
component: WorkspaceList,
|
||||
} satisfies Meta<WorkspaceListProps>;
|
||||
|
||||
export const Default = () => {
|
||||
const [items, setItems] = useState(() => {
|
||||
const items = [
|
||||
{
|
||||
id: '1',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
|
||||
'1',
|
||||
WorkspaceFlavour.LOCAL
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
|
||||
'2',
|
||||
WorkspaceFlavour.LOCAL
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
|
||||
'3',
|
||||
WorkspaceFlavour.LOCAL
|
||||
),
|
||||
},
|
||||
] satisfies WorkspaceListProps['items'];
|
||||
|
||||
items.forEach(item => {
|
||||
item.blockSuiteWorkspace.meta.setName(item.id);
|
||||
});
|
||||
|
||||
return items;
|
||||
});
|
||||
return (
|
||||
<WorkspaceList
|
||||
currentWorkspaceId={null}
|
||||
items={items}
|
||||
onClick={() => {}}
|
||||
onSettingClick={() => {}}
|
||||
onDragEnd={event => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
setItems(items => {
|
||||
const oldIndex = items.findIndex(item => item.id === active.id);
|
||||
const newIndex = items.findIndex(item => item.id === over?.id);
|
||||
|
||||
return arrayMove(items, oldIndex, newIndex);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
26
apps/storybook/tsconfig.json
Normal file
26
apps/storybook/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
// Workaround for storybook build
|
||||
"baseUrl": "../..",
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib",
|
||||
"types": ["react/experimental"]
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/component"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/env"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/workspace"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
apps/storybook/tsconfig.node.json
Normal file
21
apps/storybook/tsconfig.node.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/.storybook"
|
||||
},
|
||||
"include": [".storybook/**/*"],
|
||||
"exclude": ["lib"],
|
||||
"references": [
|
||||
{ "path": "../../packages/i18n" },
|
||||
{
|
||||
"path": "../../packages/env"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,7 +15,8 @@
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "@emotion/react",
|
||||
"incremental": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"types": ["react/experimental"]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/types/types.d.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
|
||||
Reference in New Issue
Block a user