@bbki.ng/ui 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.ts +21 -0
- package/.storybook/preview-head.html +10 -0
- package/.storybook/preview.tsx +30 -0
- package/CHANGELOG.md +8 -0
- package/README.md +124 -0
- package/package.json +57 -0
- package/scripts/build-tokens.ts +170 -0
- package/src/atoms/blink-dot/BlinkDot.stories.tsx +44 -0
- package/src/atoms/blink-dot/BlinkDot.tsx +45 -0
- package/src/atoms/blink-dot/index.ts +2 -0
- package/src/atoms/button/Button.stories.tsx +84 -0
- package/src/atoms/button/Button.tsx +59 -0
- package/src/atoms/button/Button.types.ts +20 -0
- package/src/atoms/button/Button.variants.ts +58 -0
- package/src/atoms/button/index.ts +3 -0
- package/src/atoms/link/Link.stories.tsx +121 -0
- package/src/atoms/link/Link.tsx +69 -0
- package/src/atoms/link/Link.types.ts +26 -0
- package/src/atoms/link/Link.variants.ts +55 -0
- package/src/atoms/link/index.ts +3 -0
- package/src/atoms/logo/Logo.stories.tsx +37 -0
- package/src/atoms/logo/Logo.tsx +49 -0
- package/src/atoms/logo/Logo.types.ts +4 -0
- package/src/atoms/logo/index.ts +2 -0
- package/src/index.ts +54 -0
- package/src/layout/container/Container.stories.tsx +73 -0
- package/src/layout/container/Container.tsx +57 -0
- package/src/layout/container/index.ts +2 -0
- package/src/layout/grid/Grid.stories.tsx +106 -0
- package/src/layout/grid/Grid.tsx +86 -0
- package/src/layout/grid/index.ts +2 -0
- package/src/layout/index.ts +4 -0
- package/src/molecules/article/Article.stories.tsx +45 -0
- package/src/molecules/article/Article.tsx +25 -0
- package/src/molecules/article/Article.types.ts +11 -0
- package/src/molecules/article/index.ts +2 -0
- package/src/molecules/breadcrumb/Breadcrumb.stories.tsx +60 -0
- package/src/molecules/breadcrumb/Breadcrumb.tsx +43 -0
- package/src/molecules/breadcrumb/Breadcrumb.types.ts +19 -0
- package/src/molecules/breadcrumb/index.ts +2 -0
- package/src/molecules/list/List.stories.tsx +84 -0
- package/src/molecules/list/List.tsx +79 -0
- package/src/molecules/list/List.types.ts +23 -0
- package/src/molecules/list/index.ts +2 -0
- package/src/molecules/nav/Nav.stories.tsx +45 -0
- package/src/molecules/nav/Nav.tsx +29 -0
- package/src/molecules/nav/Nav.types.ts +10 -0
- package/src/molecules/nav/index.ts +2 -0
- package/src/molecules/panel/Panel.stories.tsx +42 -0
- package/src/molecules/panel/Panel.tsx +27 -0
- package/src/molecules/panel/Panel.types.ts +6 -0
- package/src/molecules/panel/index.ts +2 -0
- package/src/molecules/table/Table.stories.tsx +54 -0
- package/src/molecules/table/Table.tsx +31 -0
- package/src/molecules/table/Table.types.ts +20 -0
- package/src/molecules/table/index.ts +2 -0
- package/src/organisms/canvas/Canvas.tsx +79 -0
- package/src/organisms/canvas/Canvas.types.ts +25 -0
- package/src/organisms/canvas/index.ts +3 -0
- package/src/organisms/canvas/useRenderer.ts +44 -0
- package/src/organisms/drop-image/DropImage.stories.tsx +36 -0
- package/src/organisms/drop-image/DropImage.tsx +193 -0
- package/src/organisms/drop-image/DropImage.types.ts +26 -0
- package/src/organisms/drop-image/index.ts +3 -0
- package/src/organisms/drop-image/useDropImage.ts +124 -0
- package/src/organisms/drop-image/utils.ts +1 -0
- package/src/organisms/drop-zone/DropZone.tsx +58 -0
- package/src/organisms/drop-zone/DropZone.types.ts +9 -0
- package/src/organisms/drop-zone/index.ts +2 -0
- package/src/organisms/loading-spiral/LoadingSpiral.stories.tsx +30 -0
- package/src/organisms/loading-spiral/LoadingSpiral.tsx +44 -0
- package/src/organisms/loading-spiral/LoadingSpiral.types.ts +4 -0
- package/src/organisms/loading-spiral/constants.ts +62 -0
- package/src/organisms/loading-spiral/createOptions.ts +31 -0
- package/src/organisms/loading-spiral/createSettings.ts +26 -0
- package/src/organisms/loading-spiral/index.ts +2 -0
- package/src/organisms/loading-spiral/useCanvasRef.ts +23 -0
- package/src/organisms/loading-spiral/utils.ts +5 -0
- package/src/organisms/page/Page.stories.tsx +65 -0
- package/src/organisms/page/Page.tsx +71 -0
- package/src/organisms/page/Page.types.ts +23 -0
- package/src/organisms/page/index.ts +8 -0
- package/src/styles.css +151 -0
- package/src/theme/ThemeContext.tsx +20 -0
- package/src/theme/ThemeProvider.tsx +93 -0
- package/src/theme/index.ts +3 -0
- package/src/tokens/css/dark.css +111 -0
- package/src/tokens/css/light.css +111 -0
- package/src/tokens/index.ts +127 -0
- package/tokens/base/colors.json +54 -0
- package/tokens/base/shadows.json +34 -0
- package/tokens/base/spacing.json +21 -0
- package/tokens/base/typography.json +35 -0
- package/tokens/semantic/dark.json +50 -0
- package/tokens/semantic/light.json +54 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +44 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Grid } from './Grid';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Grid> = {
|
|
5
|
+
title: 'Layout/Grid',
|
|
6
|
+
component: Grid,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
cols: {
|
|
10
|
+
control: 'select',
|
|
11
|
+
options: [1, 2, 3, 4, 6, 12],
|
|
12
|
+
},
|
|
13
|
+
gap: {
|
|
14
|
+
control: 'select',
|
|
15
|
+
options: ['none', 'sm', 'md', 'lg', 'xl'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
const Box = ({ children }: { children: React.ReactNode }) => (
|
|
24
|
+
<div className="h-24 bg-[var(--semantic-color-muted)] rounded flex items-center justify-center text-[var(--semantic-color-foreground)]">
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
children: (
|
|
32
|
+
<>
|
|
33
|
+
<Box>1</Box>
|
|
34
|
+
<Box>2</Box>
|
|
35
|
+
<Box>3</Box>
|
|
36
|
+
<Box>4</Box>
|
|
37
|
+
<Box>5</Box>
|
|
38
|
+
<Box>6</Box>
|
|
39
|
+
</>
|
|
40
|
+
),
|
|
41
|
+
cols: 3,
|
|
42
|
+
gap: 'md',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const ColumnVariants: Story = {
|
|
47
|
+
render: () => (
|
|
48
|
+
<div className="space-y-8">
|
|
49
|
+
{[2, 3, 4, 6].map(colCount => (
|
|
50
|
+
<div key={colCount}>
|
|
51
|
+
<p className="text-sm text-[var(--semantic-color-muted-foreground)] mb-2">
|
|
52
|
+
{colCount} columns
|
|
53
|
+
</p>
|
|
54
|
+
<Grid cols={colCount as 2 | 3 | 4 | 6} gap="md">
|
|
55
|
+
{Array.from({ length: colCount }).map((_, i) => (
|
|
56
|
+
<Box key={i}>{i + 1}</Box>
|
|
57
|
+
))}
|
|
58
|
+
</Grid>
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const GapVariations: Story = {
|
|
66
|
+
render: () => (
|
|
67
|
+
<div className="space-y-8">
|
|
68
|
+
{(['none', 'sm', 'md', 'lg', 'xl'] as const).map(gapSize => (
|
|
69
|
+
<div key={gapSize}>
|
|
70
|
+
<p className="text-sm text-[var(--semantic-color-muted-foreground)] mb-2">
|
|
71
|
+
gap: {gapSize}
|
|
72
|
+
</p>
|
|
73
|
+
<Grid cols={3} gap={gapSize}>
|
|
74
|
+
<Box>1</Box>
|
|
75
|
+
<Box>2</Box>
|
|
76
|
+
<Box>3</Box>
|
|
77
|
+
</Grid>
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const ThreeColumnLayout: Story = {
|
|
85
|
+
render: () => (
|
|
86
|
+
<div className="h-96 border border-[var(--semantic-color-border)] rounded">
|
|
87
|
+
<Grid
|
|
88
|
+
gap="md"
|
|
89
|
+
leftAside={
|
|
90
|
+
<div className="h-full bg-[var(--semantic-color-muted)] rounded p-4 text-[var(--semantic-color-foreground)]">
|
|
91
|
+
Left Sidebar
|
|
92
|
+
</div>
|
|
93
|
+
}
|
|
94
|
+
rightAside={
|
|
95
|
+
<div className="h-full bg-[var(--semantic-color-muted)] rounded p-4 text-[var(--semantic-color-foreground)]">
|
|
96
|
+
Right Sidebar
|
|
97
|
+
</div>
|
|
98
|
+
}
|
|
99
|
+
>
|
|
100
|
+
<div className="h-full bg-[var(--semantic-color-primary)]/10 rounded p-4 text-[var(--semantic-color-foreground)]">
|
|
101
|
+
Main Content Area
|
|
102
|
+
</div>
|
|
103
|
+
</Grid>
|
|
104
|
+
</div>
|
|
105
|
+
),
|
|
106
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export interface GridProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
/** 列数 */
|
|
8
|
+
cols?: 1 | 2 | 3 | 4 | 6 | 12;
|
|
9
|
+
/** 间距 */
|
|
10
|
+
gap?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
|
11
|
+
/** 响应式:移动端列数 */
|
|
12
|
+
mobileCols?: 1 | 2;
|
|
13
|
+
/** 左侧固定内容(三栏布局) */
|
|
14
|
+
leftAside?: React.ReactNode;
|
|
15
|
+
/** 右侧固定内容(三栏布局) */
|
|
16
|
+
rightAside?: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const gapMap = {
|
|
20
|
+
none: 'gap-0',
|
|
21
|
+
sm: 'gap-2',
|
|
22
|
+
md: 'gap-4',
|
|
23
|
+
lg: 'gap-6',
|
|
24
|
+
xl: 'gap-8',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const colsMap: Record<number, string> = {
|
|
28
|
+
1: 'grid-cols-1',
|
|
29
|
+
2: 'grid-cols-2',
|
|
30
|
+
3: 'grid-cols-3',
|
|
31
|
+
4: 'grid-cols-4',
|
|
32
|
+
6: 'grid-cols-6',
|
|
33
|
+
12: 'grid-cols-12',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Grid 布局组件
|
|
38
|
+
*
|
|
39
|
+
* 支持标准网格和三栏布局(参考 MainLayout)。
|
|
40
|
+
*/
|
|
41
|
+
export const Grid: React.FC<GridProps> = ({
|
|
42
|
+
children,
|
|
43
|
+
className,
|
|
44
|
+
cols = 3,
|
|
45
|
+
gap = 'md',
|
|
46
|
+
mobileCols = 1,
|
|
47
|
+
leftAside,
|
|
48
|
+
rightAside,
|
|
49
|
+
}) => {
|
|
50
|
+
// 三栏布局模式
|
|
51
|
+
if (leftAside || rightAside) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
className={twMerge(
|
|
55
|
+
'grid w-full h-full',
|
|
56
|
+
'grid-cols-1 md:grid-cols-3',
|
|
57
|
+
gapMap[gap],
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{leftAside && <div className="hidden md:block h-full overflow-auto">{leftAside}</div>}
|
|
62
|
+
<div className="h-full overflow-auto px-4">{children}</div>
|
|
63
|
+
{rightAside && <div className="hidden md:block h-full overflow-auto">{rightAside}</div>}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 标准网格模式
|
|
69
|
+
const responsiveCols = mobileCols === 2 ? 'grid-cols-2' : 'grid-cols-1';
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
className={twMerge(
|
|
74
|
+
'grid w-full',
|
|
75
|
+
responsiveCols,
|
|
76
|
+
`md:${colsMap[cols]}`,
|
|
77
|
+
gapMap[gap],
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
Grid.displayName = 'Grid';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Article } from './Article';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Article> = {
|
|
5
|
+
title: 'Molecules/Article',
|
|
6
|
+
component: Article,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
args: {
|
|
18
|
+
title: '文章标题',
|
|
19
|
+
children: <p className="text-content-primary">这是文章内容。可以包含任意 React 节点。</p>,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const WithDate: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
title: '带日期的文章',
|
|
26
|
+
date: '2024-01-15',
|
|
27
|
+
children: <p className="text-content-primary">这篇文章带有发布日期。</p>,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const WithDescription: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
title: '带描述的文章',
|
|
34
|
+
description: '这是文章的描述/摘要部分',
|
|
35
|
+
children: <p className="text-content-primary">这是文章的详细内容。</p>,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Loading: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
title: '加载中',
|
|
42
|
+
loading: true,
|
|
43
|
+
children: <p className="text-content-secondary">内容正在加载中...</p>,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { twMerge } from 'tailwind-merge';
|
|
2
|
+
import { BlinkDot } from '../../atoms/blink-dot';
|
|
3
|
+
import { ArticleProps } from './Article.types';
|
|
4
|
+
|
|
5
|
+
export const Article = (props: ArticleProps) => {
|
|
6
|
+
const { title, content, children, date, className, description, loading } = props;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className={twMerge('relative', className)}>
|
|
10
|
+
<div className="mb-32 leading-none">
|
|
11
|
+
<span className="text-2xl mb-2 inline-block text-content-primary">{title}</span>
|
|
12
|
+
{loading && title && <BlinkDot status="blink" className="ml-2" />}
|
|
13
|
+
{date && (
|
|
14
|
+
<div className="px-2 pb-0 text-content-secondary">
|
|
15
|
+
<small>{date}</small>
|
|
16
|
+
</div>
|
|
17
|
+
)}
|
|
18
|
+
</div>
|
|
19
|
+
{description && <div className="mb-8 text-content-secondary">{description}</div>}
|
|
20
|
+
<div className="text-content-primary relative">{children || content}</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
Article.displayName = 'Article';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
|
+
import { Breadcrumb } from './Breadcrumb';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Breadcrumb> = {
|
|
6
|
+
title: 'Molecules/Breadcrumb',
|
|
7
|
+
component: Breadcrumb,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
decorators: [
|
|
10
|
+
Story => (
|
|
11
|
+
<MemoryRouter>
|
|
12
|
+
<Story />
|
|
13
|
+
</MemoryRouter>
|
|
14
|
+
),
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof Breadcrumb>;
|
|
20
|
+
|
|
21
|
+
const defaultPaths = [{ name: '~', path: '/' }, { name: 'ext', path: '/ext' }, { name: 'txt' }];
|
|
22
|
+
|
|
23
|
+
const pathsWithCn = [
|
|
24
|
+
{ name: '~', path: '/' },
|
|
25
|
+
{ name: 'ext', path: '/ext' },
|
|
26
|
+
{ name: 'txt', path: '/txt' },
|
|
27
|
+
{ name: '与或非禁区' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const pathsWithMultiCnWords = [
|
|
31
|
+
{ name: '~', path: '/' },
|
|
32
|
+
{ name: 'ext', path: '/ext' },
|
|
33
|
+
{ name: '图片', path: '/png' },
|
|
34
|
+
{ name: '县城' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
paths: defaultPaths,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const WithChineseWords: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
paths: pathsWithCn,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const WithMultiChineseWords: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
paths: pathsWithMultiCnWords,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Loading: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
paths: defaultPaths,
|
|
58
|
+
loading: true,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { Link } from '../../atoms/link';
|
|
4
|
+
import { BreadcrumbProps } from './Breadcrumb.types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Breadcrumb component for displaying navigation hierarchy
|
|
8
|
+
*
|
|
9
|
+
* Uses semantic color tokens and supports loading states.
|
|
10
|
+
* Automatically adjusts vertical offset for non-English characters.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Breadcrumb paths={[{ name: '~', path: '/' }, { name: 'ext', path: '/ext' }, { name: 'txt' }]} />
|
|
14
|
+
*/
|
|
15
|
+
export const Breadcrumb: React.FC<BreadcrumbProps> = ({ paths, loading }) => {
|
|
16
|
+
const PathElements = paths.map(({ path, name }, index) => {
|
|
17
|
+
// Separator slash between segments
|
|
18
|
+
const slash = index === 0 ? null : <span className="text-content-disabled">/</span>;
|
|
19
|
+
|
|
20
|
+
// Check if name contains non-English characters
|
|
21
|
+
const isNonEnName = !/^[a-zA-Z~]+$/.test(name);
|
|
22
|
+
const offsetCls = classNames({ 'relative top-[2px]': isNonEnName });
|
|
23
|
+
|
|
24
|
+
// Determine if this is the last segment
|
|
25
|
+
const isLast = index === paths.length - 1;
|
|
26
|
+
|
|
27
|
+
// Set status for loading indicator on the last segment
|
|
28
|
+
const status = loading && isLast ? 'blink' : 'hidden';
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<span key={path || name}>
|
|
32
|
+
{slash}
|
|
33
|
+
<Link to={path ?? ''} className={offsetCls} readonly={!path} status={status}>
|
|
34
|
+
{name}
|
|
35
|
+
</Link>
|
|
36
|
+
</span>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return <div className="breadcrumb">{PathElements}</div>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
Breadcrumb.displayName = 'Breadcrumb';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path object representing a single breadcrumb segment
|
|
3
|
+
*/
|
|
4
|
+
export type PathObj = {
|
|
5
|
+
/** Optional path for navigation. If not provided, the segment is displayed as text */
|
|
6
|
+
path?: string;
|
|
7
|
+
/** Display name for the breadcrumb segment */
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Props for the Breadcrumb component
|
|
13
|
+
*/
|
|
14
|
+
export type BreadcrumbProps = {
|
|
15
|
+
/** Array of path objects to display as breadcrumbs */
|
|
16
|
+
paths: PathObj[];
|
|
17
|
+
/** Whether the last breadcrumb segment is in loading state */
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
|
+
import { List, TitledList, LinkList } from './List';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof List> = {
|
|
6
|
+
title: 'Molecules/List',
|
|
7
|
+
component: List,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'centered',
|
|
11
|
+
},
|
|
12
|
+
decorators: [
|
|
13
|
+
Story => (
|
|
14
|
+
<MemoryRouter>
|
|
15
|
+
<Story />
|
|
16
|
+
</MemoryRouter>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
const sampleItems = [
|
|
25
|
+
{ id: 1, name: 'Item 1' },
|
|
26
|
+
{ id: 2, name: 'Item 2' },
|
|
27
|
+
{ id: 3, name: 'Item 3' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const Default: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
items: sampleItems,
|
|
33
|
+
itemRenderer: (item: (typeof sampleItems)[0]) => (
|
|
34
|
+
<span className="text-content-primary">{item.name}</span>
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Horizontal: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
items: sampleItems,
|
|
42
|
+
horizontal: true,
|
|
43
|
+
itemRenderer: (item: (typeof sampleItems)[0]) => (
|
|
44
|
+
<span className="text-content-primary px-2 py-1">{item.name}</span>
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const WithFooter: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
items: sampleItems,
|
|
52
|
+
footer: <span className="text-content-secondary">Footer content</span>,
|
|
53
|
+
itemRenderer: (item: (typeof sampleItems)[0]) => (
|
|
54
|
+
<span className="text-content-primary">{item.name}</span>
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const Titled: StoryObj<typeof TitledList> = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<TitledList
|
|
62
|
+
title="列表标题"
|
|
63
|
+
description="这是一个带标题的列表"
|
|
64
|
+
items={sampleItems}
|
|
65
|
+
itemRenderer={(item: (typeof sampleItems)[0]) => (
|
|
66
|
+
<span className="text-content-primary">{item.name}</span>
|
|
67
|
+
)}
|
|
68
|
+
/>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Links: StoryObj<typeof LinkList> = {
|
|
73
|
+
render: () => (
|
|
74
|
+
<LinkList
|
|
75
|
+
title="链接列表"
|
|
76
|
+
description="常用链接"
|
|
77
|
+
links={[
|
|
78
|
+
{ to: '/', children: '首页' },
|
|
79
|
+
{ to: '/about', children: '关于' },
|
|
80
|
+
{ to: 'https://github.com', children: 'GitHub', external: true },
|
|
81
|
+
]}
|
|
82
|
+
/>
|
|
83
|
+
),
|
|
84
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { twMerge } from 'tailwind-merge';
|
|
2
|
+
import { Link, LinkProps } from '../../atoms/link';
|
|
3
|
+
import { Article } from '../article';
|
|
4
|
+
import { ListProps, TitledListProps, LinkListProps } from './List.types';
|
|
5
|
+
|
|
6
|
+
export const List = (props: ListProps) => {
|
|
7
|
+
const { items, itemRenderer, className, horizontal, compact, footer, spaceBetween } = props;
|
|
8
|
+
|
|
9
|
+
const spaceCls = compact ? '' : horizontal ? 'mr-6' : 'mb-8';
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<ul
|
|
13
|
+
className={twMerge(
|
|
14
|
+
'list-none',
|
|
15
|
+
horizontal && 'flex items-center',
|
|
16
|
+
!horizontal && footer && spaceBetween && 'flex flex-col justify-between',
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
>
|
|
20
|
+
{items.map((item, index) => {
|
|
21
|
+
return (
|
|
22
|
+
<li
|
|
23
|
+
key={item.id || index}
|
|
24
|
+
className={twMerge('shrink-0', spaceCls, horizontal && 'my-0')}
|
|
25
|
+
>
|
|
26
|
+
{itemRenderer(item, index)}
|
|
27
|
+
</li>
|
|
28
|
+
);
|
|
29
|
+
})}
|
|
30
|
+
{footer && (
|
|
31
|
+
<li key="footer" className={twMerge('shrink-0', horizontal && 'my-0')}>
|
|
32
|
+
{footer}
|
|
33
|
+
</li>
|
|
34
|
+
)}
|
|
35
|
+
</ul>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
List.displayName = 'List';
|
|
40
|
+
|
|
41
|
+
export const TitledList = (props: TitledListProps) => {
|
|
42
|
+
const { title, description, ...rest } = props;
|
|
43
|
+
|
|
44
|
+
if (!title) {
|
|
45
|
+
return <List {...rest} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Article title={title} description={description} className="w-fit">
|
|
50
|
+
<List {...rest} />
|
|
51
|
+
</Article>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
TitledList.displayName = 'TitledList';
|
|
56
|
+
|
|
57
|
+
export const LinkList = (props: LinkListProps) => {
|
|
58
|
+
const { title, description, links, ...rest } = props;
|
|
59
|
+
|
|
60
|
+
const renderLink = ({ children, to, external, ...linkRest }: LinkProps) => {
|
|
61
|
+
return (
|
|
62
|
+
<Link to={to || ''} external={external} {...linkRest}>
|
|
63
|
+
{children}
|
|
64
|
+
</Link>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<TitledList
|
|
70
|
+
title={title}
|
|
71
|
+
description={description}
|
|
72
|
+
items={links}
|
|
73
|
+
itemRenderer={renderLink}
|
|
74
|
+
{...rest}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
LinkList.displayName = 'LinkList';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ReactElement, ReactNode } from 'react';
|
|
2
|
+
import { LinkProps } from '../../atoms/link';
|
|
3
|
+
|
|
4
|
+
export interface ListProps {
|
|
5
|
+
items: any[];
|
|
6
|
+
itemRenderer: (itemProps: any, index: number) => ReactElement;
|
|
7
|
+
className?: string;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
horizontal?: boolean;
|
|
10
|
+
footer?: ReactNode;
|
|
11
|
+
spaceBetween?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TitledListProps extends ListProps {
|
|
15
|
+
title?: ReactNode;
|
|
16
|
+
description?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LinkListProps extends Omit<ListProps, 'itemRenderer' | 'items'> {
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: ReactNode;
|
|
22
|
+
links: LinkProps[];
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
|
+
import { Nav } from './Nav';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Nav> = {
|
|
6
|
+
title: 'Molecules/Nav',
|
|
7
|
+
component: Nav,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'centered',
|
|
11
|
+
},
|
|
12
|
+
decorators: [
|
|
13
|
+
Story => (
|
|
14
|
+
<MemoryRouter>
|
|
15
|
+
<Story />
|
|
16
|
+
</MemoryRouter>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
paths: [{ name: '~', path: '/' }, { name: 'ext', path: '/ext' }, { name: 'txt' }],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Loading: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
paths: [
|
|
33
|
+
{ name: '~', path: '/' },
|
|
34
|
+
{ name: 'loading', path: '/loading' },
|
|
35
|
+
],
|
|
36
|
+
loading: true,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Mini: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
paths: [{ name: 'ext', path: '/ext' }, { name: 'txt' }],
|
|
43
|
+
mini: true,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { twMerge } from 'tailwind-merge';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { Breadcrumb } from '../breadcrumb';
|
|
4
|
+
import { Logo } from '../../atoms/logo';
|
|
5
|
+
import { NavProps } from './Nav.types';
|
|
6
|
+
|
|
7
|
+
export const Nav = (props: NavProps) => {
|
|
8
|
+
const { paths, loading, mini, className, customLogo } = props;
|
|
9
|
+
const nav = useNavigate();
|
|
10
|
+
|
|
11
|
+
if (mini) {
|
|
12
|
+
return (
|
|
13
|
+
<div className={twMerge('p-2 w-full flex items-center', className)}>
|
|
14
|
+
<Breadcrumb paths={paths} />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={twMerge('p-2 w-full flex items-center', className)}>
|
|
21
|
+
{customLogo || (
|
|
22
|
+
<Logo className="mr-2 cursor-pointer hover:opacity-80" onClick={() => nav('/')} />
|
|
23
|
+
)}
|
|
24
|
+
<Breadcrumb paths={paths} loading={loading} />
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
Nav.displayName = 'Nav';
|