@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.
Files changed (97) hide show
  1. package/.storybook/main.ts +21 -0
  2. package/.storybook/preview-head.html +10 -0
  3. package/.storybook/preview.tsx +30 -0
  4. package/CHANGELOG.md +8 -0
  5. package/README.md +124 -0
  6. package/package.json +57 -0
  7. package/scripts/build-tokens.ts +170 -0
  8. package/src/atoms/blink-dot/BlinkDot.stories.tsx +44 -0
  9. package/src/atoms/blink-dot/BlinkDot.tsx +45 -0
  10. package/src/atoms/blink-dot/index.ts +2 -0
  11. package/src/atoms/button/Button.stories.tsx +84 -0
  12. package/src/atoms/button/Button.tsx +59 -0
  13. package/src/atoms/button/Button.types.ts +20 -0
  14. package/src/atoms/button/Button.variants.ts +58 -0
  15. package/src/atoms/button/index.ts +3 -0
  16. package/src/atoms/link/Link.stories.tsx +121 -0
  17. package/src/atoms/link/Link.tsx +69 -0
  18. package/src/atoms/link/Link.types.ts +26 -0
  19. package/src/atoms/link/Link.variants.ts +55 -0
  20. package/src/atoms/link/index.ts +3 -0
  21. package/src/atoms/logo/Logo.stories.tsx +37 -0
  22. package/src/atoms/logo/Logo.tsx +49 -0
  23. package/src/atoms/logo/Logo.types.ts +4 -0
  24. package/src/atoms/logo/index.ts +2 -0
  25. package/src/index.ts +54 -0
  26. package/src/layout/container/Container.stories.tsx +73 -0
  27. package/src/layout/container/Container.tsx +57 -0
  28. package/src/layout/container/index.ts +2 -0
  29. package/src/layout/grid/Grid.stories.tsx +106 -0
  30. package/src/layout/grid/Grid.tsx +86 -0
  31. package/src/layout/grid/index.ts +2 -0
  32. package/src/layout/index.ts +4 -0
  33. package/src/molecules/article/Article.stories.tsx +45 -0
  34. package/src/molecules/article/Article.tsx +25 -0
  35. package/src/molecules/article/Article.types.ts +11 -0
  36. package/src/molecules/article/index.ts +2 -0
  37. package/src/molecules/breadcrumb/Breadcrumb.stories.tsx +60 -0
  38. package/src/molecules/breadcrumb/Breadcrumb.tsx +43 -0
  39. package/src/molecules/breadcrumb/Breadcrumb.types.ts +19 -0
  40. package/src/molecules/breadcrumb/index.ts +2 -0
  41. package/src/molecules/list/List.stories.tsx +84 -0
  42. package/src/molecules/list/List.tsx +79 -0
  43. package/src/molecules/list/List.types.ts +23 -0
  44. package/src/molecules/list/index.ts +2 -0
  45. package/src/molecules/nav/Nav.stories.tsx +45 -0
  46. package/src/molecules/nav/Nav.tsx +29 -0
  47. package/src/molecules/nav/Nav.types.ts +10 -0
  48. package/src/molecules/nav/index.ts +2 -0
  49. package/src/molecules/panel/Panel.stories.tsx +42 -0
  50. package/src/molecules/panel/Panel.tsx +27 -0
  51. package/src/molecules/panel/Panel.types.ts +6 -0
  52. package/src/molecules/panel/index.ts +2 -0
  53. package/src/molecules/table/Table.stories.tsx +54 -0
  54. package/src/molecules/table/Table.tsx +31 -0
  55. package/src/molecules/table/Table.types.ts +20 -0
  56. package/src/molecules/table/index.ts +2 -0
  57. package/src/organisms/canvas/Canvas.tsx +79 -0
  58. package/src/organisms/canvas/Canvas.types.ts +25 -0
  59. package/src/organisms/canvas/index.ts +3 -0
  60. package/src/organisms/canvas/useRenderer.ts +44 -0
  61. package/src/organisms/drop-image/DropImage.stories.tsx +36 -0
  62. package/src/organisms/drop-image/DropImage.tsx +193 -0
  63. package/src/organisms/drop-image/DropImage.types.ts +26 -0
  64. package/src/organisms/drop-image/index.ts +3 -0
  65. package/src/organisms/drop-image/useDropImage.ts +124 -0
  66. package/src/organisms/drop-image/utils.ts +1 -0
  67. package/src/organisms/drop-zone/DropZone.tsx +58 -0
  68. package/src/organisms/drop-zone/DropZone.types.ts +9 -0
  69. package/src/organisms/drop-zone/index.ts +2 -0
  70. package/src/organisms/loading-spiral/LoadingSpiral.stories.tsx +30 -0
  71. package/src/organisms/loading-spiral/LoadingSpiral.tsx +44 -0
  72. package/src/organisms/loading-spiral/LoadingSpiral.types.ts +4 -0
  73. package/src/organisms/loading-spiral/constants.ts +62 -0
  74. package/src/organisms/loading-spiral/createOptions.ts +31 -0
  75. package/src/organisms/loading-spiral/createSettings.ts +26 -0
  76. package/src/organisms/loading-spiral/index.ts +2 -0
  77. package/src/organisms/loading-spiral/useCanvasRef.ts +23 -0
  78. package/src/organisms/loading-spiral/utils.ts +5 -0
  79. package/src/organisms/page/Page.stories.tsx +65 -0
  80. package/src/organisms/page/Page.tsx +71 -0
  81. package/src/organisms/page/Page.types.ts +23 -0
  82. package/src/organisms/page/index.ts +8 -0
  83. package/src/styles.css +151 -0
  84. package/src/theme/ThemeContext.tsx +20 -0
  85. package/src/theme/ThemeProvider.tsx +93 -0
  86. package/src/theme/index.ts +3 -0
  87. package/src/tokens/css/dark.css +111 -0
  88. package/src/tokens/css/light.css +111 -0
  89. package/src/tokens/index.ts +127 -0
  90. package/tokens/base/colors.json +54 -0
  91. package/tokens/base/shadows.json +34 -0
  92. package/tokens/base/spacing.json +21 -0
  93. package/tokens/base/typography.json +35 -0
  94. package/tokens/semantic/dark.json +50 -0
  95. package/tokens/semantic/light.json +54 -0
  96. package/tsconfig.json +22 -0
  97. package/vite.config.ts +44 -0
@@ -0,0 +1,21 @@
1
+ import type { StorybookConfig } from '@storybook/react-vite';
2
+ import path from 'path';
3
+
4
+ const config: StorybookConfig = {
5
+ stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
6
+ addons: ['@storybook/addon-essentials'],
7
+ framework: {
8
+ name: '@storybook/react-vite',
9
+ options: {},
10
+ },
11
+ viteFinal: async config => {
12
+ config.resolve = config.resolve || {};
13
+ config.resolve.alias = {
14
+ ...config.resolve.alias,
15
+ '@': path.resolve(__dirname, '../src'),
16
+ };
17
+ return config;
18
+ },
19
+ };
20
+
21
+ export default config;
@@ -0,0 +1,10 @@
1
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
2
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
3
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC&display=swap" rel="stylesheet" />
4
+ <style>
5
+ .noto-serif {
6
+ font-family: 'Menlo', 'Consolas', 'Noto Serif SC', monospace;
7
+ font-weight: 400;
8
+ font-style: normal;
9
+ }
10
+ </style>
@@ -0,0 +1,30 @@
1
+ import type { Preview } from '@storybook/react';
2
+ import React from 'react';
3
+ import { ThemeProvider } from '../src/theme';
4
+ import '../src/styles.css';
5
+
6
+ const preview: Preview = {
7
+ parameters: {
8
+ actions: { argTypesRegex: '^on[A-Z].*' },
9
+ controls: {
10
+ matchers: {
11
+ color: /(background|color)$/i,
12
+ date: /Date$/,
13
+ },
14
+ },
15
+ backgrounds: {
16
+ disable: true,
17
+ },
18
+ },
19
+ decorators: [
20
+ Story => (
21
+ <ThemeProvider>
22
+ <div className="p-8 noto-serif">
23
+ <Story />
24
+ </div>
25
+ </ThemeProvider>
26
+ ),
27
+ ],
28
+ };
29
+
30
+ export default preview;
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # @bbki.ng/ui
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 282af74: update ui components
8
+ - 558e40b: update compoennts
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @bbki.ng/ui
2
+
3
+ 设计系统组件库 - 基于原子设计方法论和 CSS 变量的主题系统。
4
+
5
+ ## 架构特点
6
+
7
+ - **设计令牌层**: W3C DTCG 格式的 JSON 令牌,作为单一真相源
8
+ - **CSS 变量主题**: 通过 CSS 变量实现动态主题切换,支持 light/dark/system
9
+ - **原子设计分层**: Atoms → Layout,强制依赖方向
10
+ - **Tree-shaking**: 独立模块输出,按需导入
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ pnpm add @bbki.ng/ui
16
+ ```
17
+
18
+ ## 使用
19
+
20
+ ### 基础用法
21
+
22
+ ```tsx
23
+ import { ThemeProvider, Button, Container } from '@bbki.ng/ui';
24
+ import '@bbki.ng/ui/styles';
25
+
26
+ function App() {
27
+ return (
28
+ <ThemeProvider defaultTheme="light">
29
+ <Container maxWidth="lg">
30
+ <Button variant="primary" size="lg">
31
+ Get Started
32
+ </Button>
33
+ </Container>
34
+ </ThemeProvider>
35
+ );
36
+ }
37
+ ```
38
+
39
+ ### 主题切换
40
+
41
+ ```tsx
42
+ import { useTheme } from '@bbki.ng/ui';
43
+
44
+ function ThemeToggle() {
45
+ const { theme, setTheme, toggleTheme } = useTheme();
46
+
47
+ return <button onClick={toggleTheme}>Current: {theme}</button>;
48
+ }
49
+ ```
50
+
51
+ ## 组件
52
+
53
+ ### Button
54
+
55
+ ```tsx
56
+ <Button variant="default" size="md">
57
+ Click me
58
+ </Button>
59
+ ```
60
+
61
+ Props:
62
+
63
+ - `variant`: 'default' | 'primary' | 'danger' | 'ghost' | 'disabled'
64
+ - `size`: 'sm' | 'md' | 'lg'
65
+ - `transparent`: boolean
66
+ - `loading`: boolean
67
+ - `disabled`: boolean
68
+
69
+ **注意**: 点击时有 280ms 的延迟动画效果,与旧版 Button 行为一致。
70
+
71
+ ### Container
72
+
73
+ ```tsx
74
+ <Container maxWidth="lg" padding="md" centered>
75
+ Content
76
+ </Container>
77
+ ```
78
+
79
+ ### Grid
80
+
81
+ ```tsx
82
+ // 标准网格
83
+ <Grid cols={3} gap="md">...items...</Grid>
84
+
85
+ // 三栏布局
86
+ <Grid leftAside={...} rightAside={...}>
87
+ Main content
88
+ </Grid>
89
+ ```
90
+
91
+ ## 开发
92
+
93
+ ```bash
94
+ # 开发模式
95
+ pnpm dev
96
+
97
+ # 构建令牌
98
+ pnpm build:tokens
99
+
100
+ # 构建包
101
+ pnpm build
102
+
103
+ # 启动 Storybook
104
+ pnpm storybook
105
+
106
+ # 类型检查
107
+ pnpm typecheck
108
+ ```
109
+
110
+ ## 设计令牌
111
+
112
+ 令牌存储在 `tokens/` 目录下:
113
+
114
+ - `tokens/base/`: 基础令牌(颜色、间距、字体、阴影)
115
+ - `tokens/semantic/`: 语义令牌(light/dark 主题映射)
116
+
117
+ 修改令牌后运行 `pnpm build:tokens` 生成 CSS 变量。
118
+
119
+ ## 设计原则
120
+
121
+ 1. **Token 优先**: 所有数值来自设计令牌,禁止组件内硬编码
122
+ 2. **CSS 变量主题**: 通过 CSS 变量切换主题,而非 Tailwind 的 dark: 前缀
123
+ 3. **类型安全**: Props 继承原生 HTML 属性
124
+ 4. **Tree-shaking**: 按需导入,不使用的组件不会打包
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@bbki.ng/ui",
3
+ "version": "0.1.1",
4
+ "description": "Design system component library for bbki.ng",
5
+ "type": "module",
6
+ "sideEffects": [
7
+ "*.css"
8
+ ],
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "development": "./src/index.ts",
13
+ "import": "./dist/index.mjs"
14
+ },
15
+ "./styles": {
16
+ "import": "./dist/styles.css",
17
+ "default": "./dist/styles.css"
18
+ },
19
+ "./tokens/*": {
20
+ "import": "./src/tokens/css/*"
21
+ }
22
+ },
23
+ "dependencies": {
24
+ "class-variance-authority": "^0.7.0",
25
+ "tailwind-merge": "^2.5.0"
26
+ },
27
+ "devDependencies": {
28
+ "@storybook/addon-essentials": "^8.6.0",
29
+ "@storybook/react": "^8.6.0",
30
+ "@storybook/react-vite": "^8.6.0",
31
+ "@tailwindcss/vite": "^4.1.17",
32
+ "@types/react": "^18.0.0",
33
+ "@vitejs/plugin-react": "^4.0.0",
34
+ "react": "^18.0.0",
35
+ "react-dom": "^18.0.0",
36
+ "storybook": "^8.6.0",
37
+ "style-dictionary": "^4.0.0",
38
+ "tailwindcss": "^4.1.17",
39
+ "tsx": "^4.0.0",
40
+ "typescript": "^5.0.0",
41
+ "vite": "^5.0.0",
42
+ "vite-plugin-dts": "^4.0.0"
43
+ },
44
+ "peerDependencies": {
45
+ "react": "^18.0.0",
46
+ "react-dom": "^18.0.0"
47
+ },
48
+ "scripts": {
49
+ "build": "pnpm build:tokens && vite build",
50
+ "build:tokens": "tsx scripts/build-tokens.ts",
51
+ "dev": "vite dev",
52
+ "storybook": "storybook dev -p 6006",
53
+ "build-storybook": "storybook build",
54
+ "typecheck": "tsc --noEmit",
55
+ "lint": "eslint src --ext .ts,.tsx"
56
+ }
57
+ }
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ import StyleDictionary from 'style-dictionary';
3
+ import type { Config, TransformedToken, Dictionary } from 'style-dictionary';
4
+ import { promises as fs } from 'fs';
5
+ import path from 'path';
6
+
7
+ // 获取 token 引用的实际值
8
+ function getTokenValue(token: TransformedToken, dictionary: Dictionary): string {
9
+ // 如果已经是解析后的值,直接返回
10
+ if (typeof token.value === 'string' && !token.value.startsWith('{')) {
11
+ return token.value;
12
+ }
13
+
14
+ // 处理引用格式 {color.gray.50}
15
+ if (typeof token.value === 'string' && token.value.startsWith('{')) {
16
+ const refPath = token.value.replace(/[{}]/g, '').split('.');
17
+ const refToken = dictionary.tokens;
18
+
19
+ // 递归查找引用的 token
20
+ let current: unknown = refToken;
21
+ for (const key of refPath) {
22
+ if (current && typeof current === 'object' && key in current) {
23
+ current = (current as Record<string, unknown>)[key];
24
+ } else {
25
+ return token.value; // 找不到引用,返回原值
26
+ }
27
+ }
28
+
29
+ // 如果找到的是 token 对象,获取其 value
30
+ if (current && typeof current === 'object' && current !== null) {
31
+ const tokenObj = current as { value?: string };
32
+ if (tokenObj.value) {
33
+ return tokenObj.value;
34
+ }
35
+ }
36
+ }
37
+
38
+ return token.value;
39
+ }
40
+
41
+ // 自定义格式:生成 CSS 变量,解析所有引用
42
+ StyleDictionary.registerFormat({
43
+ name: 'css/variables-resolved',
44
+ format: ({ dictionary }) => {
45
+ const variables = dictionary.allTokens
46
+ .map((token: TransformedToken) => {
47
+ const value = getTokenValue(token, dictionary);
48
+ return ` --${token.name}: ${value};`;
49
+ })
50
+ .join('\n');
51
+
52
+ return `:root {\n${variables}\n}`;
53
+ },
54
+ });
55
+
56
+ // 自定义格式:生成 TypeScript 配置
57
+ StyleDictionary.registerFormat({
58
+ name: 'typescript/theme-config',
59
+ format: ({ dictionary }) => {
60
+ const semanticColors = dictionary.allTokens
61
+ .filter((t: TransformedToken) => t.path[0] === 'semantic' && t.path[1] === 'color')
62
+ .reduce(
63
+ (acc: Record<string, string>, token: TransformedToken) => {
64
+ const name = token.path.slice(2).join('-');
65
+ acc[name] = `var(--${token.name})`;
66
+ return acc;
67
+ },
68
+ {} as Record<string, string>
69
+ );
70
+
71
+ const baseTokens = dictionary.allTokens
72
+ .filter((t: TransformedToken) => t.path[0] !== 'semantic')
73
+ .reduce(
74
+ (acc: Record<string, Record<string, string>>, token: TransformedToken) => {
75
+ const category = token.path[0] as string;
76
+ const name = token.path.slice(1).join('-');
77
+ if (!acc[category]) acc[category] = {};
78
+ acc[category][name] = `var(--${token.name})`;
79
+ return acc;
80
+ },
81
+ {} as Record<string, Record<string, string>>
82
+ );
83
+
84
+ return `/**
85
+ * Auto-generated from design tokens
86
+ * Do not edit manually
87
+ */
88
+ export const tokens = {
89
+ colors: ${JSON.stringify(semanticColors, null, 2).replace(/"/g, "'")},
90
+ base: ${JSON.stringify(baseTokens, null, 2).replace(/"/g, "'")},
91
+ } as const;
92
+
93
+ export type ThemeTokens = typeof tokens;
94
+ `;
95
+ },
96
+ });
97
+
98
+ async function ensureDir(dir: string) {
99
+ await fs.mkdir(dir, { recursive: true });
100
+ }
101
+
102
+ async function build() {
103
+ const basePath = process.cwd();
104
+ const srcTokensPath = path.join(basePath, 'src', 'tokens');
105
+ const cssPath = path.join(srcTokensPath, 'css');
106
+
107
+ await ensureDir(cssPath);
108
+
109
+ // Light theme config - 解析所有引用
110
+ const lightConfig: Config = {
111
+ log: { verbosity: 'verbose' },
112
+ source: ['tokens/base/**/*.json', 'tokens/semantic/light.json'],
113
+ platforms: {
114
+ css: {
115
+ transformGroup: 'css',
116
+ transforms: ['attribute/cti', 'name/kebab'],
117
+ buildPath: 'src/tokens/css/',
118
+ files: [
119
+ {
120
+ destination: 'light.css',
121
+ format: 'css/variables-resolved',
122
+ },
123
+ ],
124
+ },
125
+ typescript: {
126
+ transformGroup: 'js',
127
+ transforms: ['attribute/cti', 'name/kebab'],
128
+ buildPath: 'src/tokens/',
129
+ files: [
130
+ {
131
+ destination: 'index.ts',
132
+ format: 'typescript/theme-config',
133
+ },
134
+ ],
135
+ },
136
+ },
137
+ };
138
+
139
+ // Dark theme config - 解析所有引用
140
+ const darkConfig: Config = {
141
+ log: { verbosity: 'verbose' },
142
+ source: ['tokens/base/**/*.json', 'tokens/semantic/dark.json'],
143
+ platforms: {
144
+ css: {
145
+ transformGroup: 'css',
146
+ transforms: ['attribute/cti', 'name/kebab'],
147
+ buildPath: 'src/tokens/css/',
148
+ files: [
149
+ {
150
+ destination: 'dark.css',
151
+ format: 'css/variables-resolved',
152
+ },
153
+ ],
154
+ },
155
+ },
156
+ };
157
+
158
+ const sd = new StyleDictionary(lightConfig);
159
+ await sd.buildAllPlatforms();
160
+
161
+ const sdDark = new StyleDictionary(darkConfig);
162
+ await sdDark.buildAllPlatforms();
163
+
164
+ console.log('✅ Tokens built successfully');
165
+ }
166
+
167
+ build().catch(error => {
168
+ console.error('❌ Token build failed:', error);
169
+ process.exit(1);
170
+ });
@@ -0,0 +1,44 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { BlinkDot } from './BlinkDot';
3
+
4
+ const meta: Meta<typeof BlinkDot> = {
5
+ title: 'Atoms/BlinkDot',
6
+ component: BlinkDot,
7
+ parameters: {
8
+ layout: 'centered',
9
+ },
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ status: {
13
+ control: 'select',
14
+ options: ['blink', 'still', 'hidden'],
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {
23
+ args: {
24
+ status: 'blink',
25
+ },
26
+ };
27
+
28
+ export const Blink: Story = {
29
+ args: {
30
+ status: 'blink',
31
+ },
32
+ };
33
+
34
+ export const Still: Story = {
35
+ args: {
36
+ status: 'still',
37
+ },
38
+ };
39
+
40
+ export const Hidden: Story = {
41
+ args: {
42
+ status: 'hidden',
43
+ },
44
+ };
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export type BlinkDotStatus = 'blink' | 'still' | 'hidden';
5
+
6
+ export interface BlinkDotProps {
7
+ className?: string;
8
+ status?: BlinkDotStatus;
9
+ xOffset?: number; // 水平偏移,单位像素
10
+ yOffset?: number; // 垂直偏移,单位像素
11
+ }
12
+
13
+ /**
14
+ * BlinkDot 组件
15
+ *
16
+ * 用于展示状态指示器的闪烁点。
17
+ */
18
+ export const BlinkDot: React.FC<BlinkDotProps> = ({
19
+ className,
20
+ status = 'hidden',
21
+ xOffset = 0,
22
+ yOffset = -28,
23
+ }) => {
24
+ return (
25
+ <span className="inline-flex justify-center items-center relative">
26
+ <span
27
+ className={twMerge(
28
+ 'absolute inline-flex h-full w-full rounded-full',
29
+ 'text-status-error',
30
+ status === 'blink' && 'animate-ping-fast',
31
+ status === 'hidden' && 'invisible',
32
+ className
33
+ )}
34
+ style={{
35
+ left: `${xOffset}px`,
36
+ top: `${yOffset}px`,
37
+ }}
38
+ >
39
+ .
40
+ </span>
41
+ </span>
42
+ );
43
+ };
44
+
45
+ BlinkDot.displayName = 'BlinkDot';
@@ -0,0 +1,2 @@
1
+ export { BlinkDot } from './BlinkDot';
2
+ export type { BlinkDotProps, BlinkDotStatus } from './BlinkDot';
@@ -0,0 +1,84 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Button } from './Button';
3
+
4
+ const meta: Meta<typeof Button> = {
5
+ title: 'Atoms/Button',
6
+ component: Button,
7
+ parameters: {
8
+ layout: 'centered',
9
+ },
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ variant: {
13
+ control: 'select',
14
+ options: ['default', 'primary', 'danger', 'ghost', 'disabled'],
15
+ },
16
+ size: {
17
+ control: 'select',
18
+ options: ['sm', 'md', 'lg'],
19
+ },
20
+ transparent: {
21
+ control: 'boolean',
22
+ },
23
+ loading: {
24
+ control: 'boolean',
25
+ },
26
+ disabled: {
27
+ control: 'boolean',
28
+ },
29
+ },
30
+ };
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof meta>;
34
+
35
+ export const Default: Story = {
36
+ args: {
37
+ children: 'Button',
38
+ variant: 'default',
39
+ size: 'md',
40
+ },
41
+ };
42
+
43
+ export const Variants: Story = {
44
+ render: () => (
45
+ <div className="flex gap-4">
46
+ <Button variant="default">Default (Normal)</Button>
47
+ <Button variant="primary">Primary</Button>
48
+ <Button variant="danger">Danger</Button>
49
+ <Button variant="ghost">Ghost</Button>
50
+ <Button variant="disabled">Disabled</Button>
51
+ </div>
52
+ ),
53
+ };
54
+
55
+ export const Sizes: Story = {
56
+ render: () => (
57
+ <div className="flex items-center gap-4">
58
+ <Button size="sm">Small</Button>
59
+ <Button size="md">Medium</Button>
60
+ <Button size="lg">Large</Button>
61
+ </div>
62
+ ),
63
+ };
64
+
65
+ export const Transparent: Story = {
66
+ args: {
67
+ children: 'Transparent',
68
+ transparent: true,
69
+ },
70
+ };
71
+
72
+ export const Loading: Story = {
73
+ args: {
74
+ children: 'Loading',
75
+ loading: true,
76
+ },
77
+ };
78
+
79
+ export const Disabled: Story = {
80
+ args: {
81
+ children: 'Disabled',
82
+ disabled: true,
83
+ },
84
+ };
@@ -0,0 +1,59 @@
1
+ import React, { forwardRef, useCallback } from 'react';
2
+ import { buttonVariants } from './Button.variants';
3
+ import { ButtonProps } from './Button.types';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { BlinkDot } from '../blink-dot';
6
+
7
+ /**
8
+ * Button 组件
9
+ */
10
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
11
+ (
12
+ {
13
+ children,
14
+ variant = 'default',
15
+ size = 'md',
16
+ transparent = false,
17
+ loading = false,
18
+ disabled,
19
+ className,
20
+ onClick,
21
+ ...props
22
+ },
23
+ ref
24
+ ) => {
25
+ // 处理点击事件,添加 280ms 延迟动画(与旧 Button 一致)
26
+ const handleClick = useCallback(
27
+ (e: React.MouseEvent<HTMLButtonElement>) => {
28
+ if (variant === 'disabled' || disabled || loading) {
29
+ return;
30
+ }
31
+
32
+ onClick?.(e);
33
+ },
34
+ [variant, disabled, loading, onClick]
35
+ );
36
+
37
+ return (
38
+ <button
39
+ ref={ref}
40
+ className={twMerge(
41
+ buttonVariants({
42
+ variant: disabled || loading ? 'disabled' : variant,
43
+ size,
44
+ transparent: transparent,
45
+ }),
46
+ className
47
+ )}
48
+ disabled={disabled || loading}
49
+ onClick={handleClick}
50
+ {...props}
51
+ >
52
+ {children}
53
+ {loading && <BlinkDot status="blink" xOffset={2} />}
54
+ </button>
55
+ );
56
+ }
57
+ );
58
+
59
+ Button.displayName = 'Button';
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { ButtonVariants } from './Button.variants';
3
+
4
+ /**
5
+ * Button 组件 Props
6
+ * 继承原生 button 属性,避免破坏原生行为
7
+ */
8
+ export interface ButtonProps
9
+ extends
10
+ Omit<ButtonVariants, 'transparent'>,
11
+ Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'size'> {
12
+ /** 按钮内容 */
13
+ children: React.ReactNode;
14
+ /** 加载状态 */
15
+ loading?: boolean;
16
+ /** 自定义类名 */
17
+ className?: string;
18
+ /** 透明模式(无阴影) */
19
+ transparent?: boolean;
20
+ }