@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,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
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,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
|
+
}
|