gemini-uis 0.1.0
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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/deploy.yml +59 -0
- package/.husky/pre-commit +12 -0
- package/.storybook/index.css +1 -0
- package/.storybook/main.ts +27 -0
- package/.storybook/preview.ts +22 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/README.md +11 -0
- package/eslint.config.js +26 -0
- package/index.html +13 -0
- package/package.json +65 -0
- package/public/vite.svg +1 -0
- package/src/icons/LoadingIcon.tsx +34 -0
- package/src/icons/index.ts +2 -0
- package/src/icons/types.ts +6 -0
- package/src/libs/Button/guide/Button.stories.tsx +378 -0
- package/src/libs/Button/guide/README.md +144 -0
- package/src/libs/Button/index.tsx +69 -0
- package/src/libs/Button/styles.ts +71 -0
- package/src/libs/Button/types.ts +52 -0
- package/src/libs/index.ts +10 -0
- package/src/libs/styles.css +2 -0
- package/src/utils/cn.ts +8 -0
- package/src/utils/index.ts +1 -0
- package/src/vite-env.d.ts +7 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +103 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changesets
|
|
2
|
+
|
|
3
|
+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
|
4
|
+
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
|
5
|
+
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
|
6
|
+
|
|
7
|
+
We have a quick list of common questions to get you started engaging with this project in
|
|
8
|
+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
|
|
3
|
+
"changelog": "@changesets/cli/changelog",
|
|
4
|
+
"commit": false,
|
|
5
|
+
"fixed": [],
|
|
6
|
+
"linked": [],
|
|
7
|
+
"access": "restricted",
|
|
8
|
+
"baseBranch": "main",
|
|
9
|
+
"updateInternalDependencies": "patch",
|
|
10
|
+
"ignore": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Deploy Storybook to GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- production
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
pages: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Node.js
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '22'
|
|
25
|
+
|
|
26
|
+
- name: Setup pnpm
|
|
27
|
+
uses: pnpm/action-setup@v4
|
|
28
|
+
with:
|
|
29
|
+
version: 10
|
|
30
|
+
|
|
31
|
+
- name: Setup pnpm cache
|
|
32
|
+
uses: actions/cache@v4
|
|
33
|
+
with:
|
|
34
|
+
path: ~/.pnpm-store
|
|
35
|
+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
36
|
+
restore-keys: |
|
|
37
|
+
${{ runner.os }}-pnpm-store-
|
|
38
|
+
|
|
39
|
+
- name: Install dependencies
|
|
40
|
+
run: pnpm install --frozen-lockfile
|
|
41
|
+
|
|
42
|
+
- name: Build Storybook
|
|
43
|
+
run: pnpm build-storybook
|
|
44
|
+
|
|
45
|
+
- name: Create .nojekyll file
|
|
46
|
+
run: touch storybook-static/.nojekyll
|
|
47
|
+
|
|
48
|
+
- name: Upload artifact for GitHub Pages
|
|
49
|
+
uses: actions/upload-pages-artifact@v3
|
|
50
|
+
with:
|
|
51
|
+
path: storybook-static
|
|
52
|
+
|
|
53
|
+
deploy:
|
|
54
|
+
needs: build
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
|
|
57
|
+
steps:
|
|
58
|
+
- name: Deploy to GitHub Pages
|
|
59
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 运行 lint-staged 检查
|
|
2
|
+
echo "Running code quality checks..."
|
|
3
|
+
npx lint-staged
|
|
4
|
+
|
|
5
|
+
# 如果检查失败,阻止提交
|
|
6
|
+
if [ $? -ne 0 ]; then
|
|
7
|
+
echo "Code quality checks failed. Please fix the errors before committing."
|
|
8
|
+
echo "Run 'pnpm run lint' to see detailed error messages."
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
echo "Code quality checks passed. Proceeding with commit."
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StorybookConfig } from '@storybook/react-vite';
|
|
2
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
3
|
+
// import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
|
|
5
|
+
const config: StorybookConfig = {
|
|
6
|
+
stories: [
|
|
7
|
+
"../src/**/*.mdx",
|
|
8
|
+
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
|
9
|
+
],
|
|
10
|
+
addons: [
|
|
11
|
+
"@chromatic-com/storybook",
|
|
12
|
+
"@storybook/addon-vitest",
|
|
13
|
+
"@storybook/addon-a11y",
|
|
14
|
+
"@storybook/addon-docs",
|
|
15
|
+
"@storybook/addon-onboarding"
|
|
16
|
+
],
|
|
17
|
+
framework: "@storybook/react-vite",
|
|
18
|
+
async viteFinal(config) {
|
|
19
|
+
config.plugins = config.plugins || [];
|
|
20
|
+
config.plugins.push(tsconfigPaths());
|
|
21
|
+
// config.plugins.push(tailwindcss());
|
|
22
|
+
config.base = '/UIs/';
|
|
23
|
+
return config;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default config;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Preview } from '@storybook/react-vite'
|
|
2
|
+
import './index.css'
|
|
3
|
+
|
|
4
|
+
const preview: Preview = {
|
|
5
|
+
parameters: {
|
|
6
|
+
controls: {
|
|
7
|
+
matchers: {
|
|
8
|
+
color: /(background|color)$/i,
|
|
9
|
+
date: /Date$/i,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
a11y: {
|
|
14
|
+
// 'todo' - show a11y violations in the test UI only
|
|
15
|
+
// 'error' - fail CI on a11y violations
|
|
16
|
+
// 'off' - skip a11y checks entirely
|
|
17
|
+
test: 'todo'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default preview;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
|
|
2
|
+
import { setProjectAnnotations } from '@storybook/react-vite';
|
|
3
|
+
import * as projectAnnotations from './preview';
|
|
4
|
+
|
|
5
|
+
// This is an important step to apply the right configuration when testing your stories.
|
|
6
|
+
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
|
7
|
+
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
|
package/README.md
ADDED
package/eslint.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
|
2
|
+
import storybook from "eslint-plugin-storybook";
|
|
3
|
+
|
|
4
|
+
import js from '@eslint/js'
|
|
5
|
+
import globals from 'globals'
|
|
6
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
7
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
8
|
+
import tseslint from 'typescript-eslint'
|
|
9
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
10
|
+
|
|
11
|
+
export default defineConfig([
|
|
12
|
+
globalIgnores(['dist']),
|
|
13
|
+
{
|
|
14
|
+
files: ['**/*.{ts,tsx}'],
|
|
15
|
+
extends: [
|
|
16
|
+
js.configs.recommended,
|
|
17
|
+
tseslint.configs.recommended,
|
|
18
|
+
reactHooks.configs.flat.recommended,
|
|
19
|
+
reactRefresh.configs.vite,
|
|
20
|
+
],
|
|
21
|
+
languageOptions: {
|
|
22
|
+
ecmaVersion: 2020,
|
|
23
|
+
globals: globals.browser,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
])
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>gemini-uis</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gemini-uis",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": ["*.css"],
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "storybook dev -p 6006",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"build-storybook": "storybook build -o storybook-static",
|
|
12
|
+
"prepare": "husky install"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"react": "^19.2.0",
|
|
16
|
+
"react-dom": "^19.2.0"
|
|
17
|
+
},
|
|
18
|
+
"lint-staged": {
|
|
19
|
+
"*.{js,jsx,ts,tsx}": [
|
|
20
|
+
"eslint --fix"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@changesets/cli": "^2.29.8",
|
|
25
|
+
"@chromatic-com/storybook": "^4.1.3",
|
|
26
|
+
"@eslint/js": "^9.39.1",
|
|
27
|
+
"@storybook/addon-a11y": "^10.1.10",
|
|
28
|
+
"@storybook/addon-docs": "^10.1.10",
|
|
29
|
+
"@storybook/addon-onboarding": "^10.1.10",
|
|
30
|
+
"@storybook/addon-vitest": "^10.1.10",
|
|
31
|
+
"@storybook/react-vite": "^10.1.10",
|
|
32
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
33
|
+
"@types/node": "^25.0.3",
|
|
34
|
+
"@types/react": "^19.2.5",
|
|
35
|
+
"@types/react-dom": "^19.2.3",
|
|
36
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
37
|
+
"@vitest/browser-playwright": "^4.0.16",
|
|
38
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
39
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
40
|
+
"eslint": "^9.39.1",
|
|
41
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
42
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
43
|
+
"eslint-plugin-storybook": "^10.1.10",
|
|
44
|
+
"globals": "^16.5.0",
|
|
45
|
+
"husky": "^9.1.7",
|
|
46
|
+
"lint-staged": "^16.2.7",
|
|
47
|
+
"playwright": "^1.57.0",
|
|
48
|
+
"storybook": "^10.1.10",
|
|
49
|
+
"tailwindcss": "^4.1.18",
|
|
50
|
+
"typescript": "~5.9.3",
|
|
51
|
+
"typescript-eslint": "^8.46.4",
|
|
52
|
+
"vite": "npm:rolldown-vite@7.2.5",
|
|
53
|
+
"vite-plugin-dts": "^4.5.4",
|
|
54
|
+
"vite-tsconfig-paths": "^6.0.3",
|
|
55
|
+
"vitest": "^4.0.16"
|
|
56
|
+
},
|
|
57
|
+
"pnpm": {
|
|
58
|
+
"overrides": {
|
|
59
|
+
"vite": "npm:rolldown-vite@7.2.5"
|
|
60
|
+
},
|
|
61
|
+
"ignoredBuiltDependencies": [
|
|
62
|
+
"esbuild"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
package/public/vite.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
2
|
+
import type { IconProps } from './types';
|
|
3
|
+
|
|
4
|
+
export const LoadingIcon: FC<IconProps> = ({
|
|
5
|
+
size = 16,
|
|
6
|
+
className = 'animate-spin',
|
|
7
|
+
...props
|
|
8
|
+
}) => (
|
|
9
|
+
<svg
|
|
10
|
+
width={size}
|
|
11
|
+
height={size}
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
fill="none"
|
|
14
|
+
viewBox="0 0 24 24"
|
|
15
|
+
className={className}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<circle
|
|
19
|
+
className="opacity-25"
|
|
20
|
+
cx="12"
|
|
21
|
+
cy="12"
|
|
22
|
+
r="10"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
strokeWidth="4"
|
|
25
|
+
/>
|
|
26
|
+
<path
|
|
27
|
+
className="opacity-75"
|
|
28
|
+
fill="currentColor"
|
|
29
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
30
|
+
/>
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export default LoadingIcon;
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Button } from '@/libs/Button'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/Button',
|
|
6
|
+
component: Button,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: '一个精简的按钮组件,仅保留基本功能,专为简单项目使用而设计。',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
argTypes: {
|
|
17
|
+
type: {
|
|
18
|
+
control: 'select',
|
|
19
|
+
options: ['primary', 'secondary', 'outline', 'ghost', 'text', 'danger', 'default'],
|
|
20
|
+
description: '按钮变体',
|
|
21
|
+
table: {
|
|
22
|
+
type: { summary: 'ButtonType' },
|
|
23
|
+
defaultValue: { summary: 'primary' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
control: 'select',
|
|
28
|
+
options: ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
29
|
+
description: '按钮尺寸',
|
|
30
|
+
table: {
|
|
31
|
+
type: { summary: 'ButtonSize' },
|
|
32
|
+
defaultValue: { summary: 'md' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
rounded: {
|
|
36
|
+
control: 'boolean',
|
|
37
|
+
description: '是否圆角',
|
|
38
|
+
table: {
|
|
39
|
+
type: { summary: 'boolean' },
|
|
40
|
+
defaultValue: { summary: 'false' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
disabled: {
|
|
44
|
+
control: 'boolean',
|
|
45
|
+
description: '是否禁用',
|
|
46
|
+
table: {
|
|
47
|
+
type: { summary: 'boolean' },
|
|
48
|
+
defaultValue: { summary: 'false' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
loading: {
|
|
52
|
+
control: 'boolean',
|
|
53
|
+
description: '是否显示加载状态',
|
|
54
|
+
table: {
|
|
55
|
+
type: { summary: 'boolean' },
|
|
56
|
+
defaultValue: { summary: 'false' },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
htmlType: {
|
|
60
|
+
control: 'select',
|
|
61
|
+
options: ['button', 'submit', 'reset'],
|
|
62
|
+
description: 'HTML按钮类型',
|
|
63
|
+
table: {
|
|
64
|
+
type: { summary: "'button' | 'submit' | 'reset'" },
|
|
65
|
+
defaultValue: { summary: 'button' },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
children: {
|
|
69
|
+
control: 'text',
|
|
70
|
+
description: '按钮内容',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
} satisfies Meta<typeof Button>
|
|
74
|
+
|
|
75
|
+
export default meta
|
|
76
|
+
type Story = StoryObj<typeof meta>
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 基础按钮示例
|
|
80
|
+
*/
|
|
81
|
+
export const Default: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
children: '点击我',
|
|
84
|
+
type: 'primary',
|
|
85
|
+
size: 'md',
|
|
86
|
+
rounded: false,
|
|
87
|
+
disabled: false,
|
|
88
|
+
loading: false,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 所有变体展示
|
|
94
|
+
*/
|
|
95
|
+
export const Variants: Story = {
|
|
96
|
+
render: () => (
|
|
97
|
+
<div className="flex flex-wrap gap-4">
|
|
98
|
+
<Button type="primary">主要按钮</Button>
|
|
99
|
+
<Button type="secondary">次要按钮</Button>
|
|
100
|
+
<Button type="outline">轮廓按钮</Button>
|
|
101
|
+
<Button type="ghost">幽灵按钮</Button>
|
|
102
|
+
<Button type="text">文本按钮</Button>
|
|
103
|
+
<Button type="danger">危险按钮</Button>
|
|
104
|
+
<Button type="default">默认按钮</Button>
|
|
105
|
+
</div>
|
|
106
|
+
),
|
|
107
|
+
parameters: {
|
|
108
|
+
docs: {
|
|
109
|
+
description: {
|
|
110
|
+
story: '展示所有可用的按钮变体类型。',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 所有尺寸展示
|
|
118
|
+
*/
|
|
119
|
+
export const Sizes: Story = {
|
|
120
|
+
render: () => (
|
|
121
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
122
|
+
<Button size="xs">超小按钮</Button>
|
|
123
|
+
<Button size="sm">小按钮</Button>
|
|
124
|
+
<Button size="md">中等按钮</Button>
|
|
125
|
+
<Button size="lg">大按钮</Button>
|
|
126
|
+
<Button size="xl">超大按钮</Button>
|
|
127
|
+
</div>
|
|
128
|
+
),
|
|
129
|
+
parameters: {
|
|
130
|
+
docs: {
|
|
131
|
+
description: {
|
|
132
|
+
story: '展示所有可用的按钮尺寸。',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 圆角设置
|
|
140
|
+
*/
|
|
141
|
+
export const Rounded: Story = {
|
|
142
|
+
render: () => (
|
|
143
|
+
<div className="flex flex-wrap gap-4">
|
|
144
|
+
<Button rounded={false}>方框按钮</Button>
|
|
145
|
+
<Button rounded={true}>圆角按钮</Button>
|
|
146
|
+
</div>
|
|
147
|
+
),
|
|
148
|
+
parameters: {
|
|
149
|
+
docs: {
|
|
150
|
+
description: {
|
|
151
|
+
story: '展示圆角和方框两种样式。',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 状态展示
|
|
159
|
+
*/
|
|
160
|
+
export const States: Story = {
|
|
161
|
+
render: () => (
|
|
162
|
+
<div className="flex flex-wrap gap-4">
|
|
163
|
+
<Button>正常状态</Button>
|
|
164
|
+
<Button loading>加载中</Button>
|
|
165
|
+
<Button disabled>禁用</Button>
|
|
166
|
+
<Button loading disabled>加载且禁用</Button>
|
|
167
|
+
</div>
|
|
168
|
+
),
|
|
169
|
+
parameters: {
|
|
170
|
+
docs: {
|
|
171
|
+
description: {
|
|
172
|
+
story: '展示按钮的不同状态:正常、加载、禁用。',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 事件处理示例
|
|
180
|
+
*/
|
|
181
|
+
export const WithClickHandler: Story = {
|
|
182
|
+
render: () => (
|
|
183
|
+
<Button
|
|
184
|
+
onClick={(e) => {
|
|
185
|
+
e.preventDefault()
|
|
186
|
+
alert('按钮被点击了!')
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
点击我
|
|
190
|
+
</Button>
|
|
191
|
+
),
|
|
192
|
+
parameters: {
|
|
193
|
+
docs: {
|
|
194
|
+
description: {
|
|
195
|
+
story: '展示如何处理按钮点击事件。',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 表单集成示例
|
|
203
|
+
*/
|
|
204
|
+
export const FormIntegration: Story = {
|
|
205
|
+
render: () => (
|
|
206
|
+
<form
|
|
207
|
+
onSubmit={(e) => {
|
|
208
|
+
e.preventDefault()
|
|
209
|
+
alert('表单已提交')
|
|
210
|
+
}}
|
|
211
|
+
onReset={() => {
|
|
212
|
+
alert('表单已重置')
|
|
213
|
+
}}
|
|
214
|
+
className="flex gap-4"
|
|
215
|
+
>
|
|
216
|
+
<Button type="primary" htmlType="submit">
|
|
217
|
+
提交表单
|
|
218
|
+
</Button>
|
|
219
|
+
<Button type="secondary" htmlType="reset">
|
|
220
|
+
重置
|
|
221
|
+
</Button>
|
|
222
|
+
</form>
|
|
223
|
+
),
|
|
224
|
+
parameters: {
|
|
225
|
+
docs: {
|
|
226
|
+
description: {
|
|
227
|
+
story: '展示如何在表单中使用按钮,包括提交和重置功能。',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 组合示例:不同变体的所有尺寸
|
|
235
|
+
*/
|
|
236
|
+
export const AllVariantsAndSizes: Story = {
|
|
237
|
+
render: () => {
|
|
238
|
+
const variants: Array<'primary' | 'secondary' | 'outline' | 'ghost' | 'text' | 'danger' | 'default'> = [
|
|
239
|
+
'primary',
|
|
240
|
+
'secondary',
|
|
241
|
+
'outline',
|
|
242
|
+
'ghost',
|
|
243
|
+
'text',
|
|
244
|
+
'danger',
|
|
245
|
+
'default',
|
|
246
|
+
]
|
|
247
|
+
const sizes: Array<'xs' | 'sm' | 'md' | 'lg' | 'xl'> = ['xs', 'sm', 'md', 'lg', 'xl']
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div className="space-y-6">
|
|
251
|
+
{variants.map((variant) => (
|
|
252
|
+
<div key={variant} className="space-y-2">
|
|
253
|
+
<h3 className="text-sm font-semibold text-gray-700 capitalize">{variant}</h3>
|
|
254
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
255
|
+
{sizes.map((size) => (
|
|
256
|
+
<Button key={size} type={variant} size={size}>
|
|
257
|
+
{size.toUpperCase()}
|
|
258
|
+
</Button>
|
|
259
|
+
))}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
)
|
|
265
|
+
},
|
|
266
|
+
parameters: {
|
|
267
|
+
docs: {
|
|
268
|
+
description: {
|
|
269
|
+
story: '展示所有变体和尺寸的组合效果。',
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 圆角与方框对比
|
|
277
|
+
*/
|
|
278
|
+
export const RoundedComparison: Story = {
|
|
279
|
+
render: () => {
|
|
280
|
+
const variants: Array<'primary' | 'secondary' | 'outline'> = ['primary', 'secondary', 'outline']
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div className="space-y-6">
|
|
284
|
+
<div className="space-y-2">
|
|
285
|
+
<h3 className="text-sm font-semibold text-gray-700">方框按钮 (rounded=false)</h3>
|
|
286
|
+
<div className="flex flex-wrap gap-2">
|
|
287
|
+
{variants.map((variant) => (
|
|
288
|
+
<Button key={variant} type={variant} rounded={false}>
|
|
289
|
+
{variant}
|
|
290
|
+
</Button>
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
<div className="space-y-2">
|
|
295
|
+
<h3 className="text-sm font-semibold text-gray-700">圆角按钮 (rounded=true)</h3>
|
|
296
|
+
<div className="flex flex-wrap gap-2">
|
|
297
|
+
{variants.map((variant) => (
|
|
298
|
+
<Button key={variant} type={variant} rounded={true}>
|
|
299
|
+
{variant}
|
|
300
|
+
</Button>
|
|
301
|
+
))}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
)
|
|
306
|
+
},
|
|
307
|
+
parameters: {
|
|
308
|
+
docs: {
|
|
309
|
+
description: {
|
|
310
|
+
story: '对比不同变体的圆角和方框样式。',
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 加载状态示例
|
|
318
|
+
*/
|
|
319
|
+
export const LoadingStates: Story = {
|
|
320
|
+
render: () => (
|
|
321
|
+
<div className="flex flex-wrap gap-4">
|
|
322
|
+
<Button type="primary" loading>
|
|
323
|
+
加载中
|
|
324
|
+
</Button>
|
|
325
|
+
<Button type="secondary" loading>
|
|
326
|
+
加载中
|
|
327
|
+
</Button>
|
|
328
|
+
<Button type="outline" loading>
|
|
329
|
+
加载中
|
|
330
|
+
</Button>
|
|
331
|
+
<Button type="danger" loading>
|
|
332
|
+
加载中
|
|
333
|
+
</Button>
|
|
334
|
+
</div>
|
|
335
|
+
),
|
|
336
|
+
parameters: {
|
|
337
|
+
docs: {
|
|
338
|
+
description: {
|
|
339
|
+
story: '展示不同变体的加载状态。',
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 禁用状态示例
|
|
347
|
+
*/
|
|
348
|
+
export const DisabledStates: Story = {
|
|
349
|
+
render: () => (
|
|
350
|
+
<div className="flex flex-wrap gap-4">
|
|
351
|
+
<Button type="primary" disabled>
|
|
352
|
+
禁用
|
|
353
|
+
</Button>
|
|
354
|
+
<Button type="secondary" disabled>
|
|
355
|
+
禁用
|
|
356
|
+
</Button>
|
|
357
|
+
<Button type="outline" disabled>
|
|
358
|
+
禁用
|
|
359
|
+
</Button>
|
|
360
|
+
<Button type="ghost" disabled>
|
|
361
|
+
禁用
|
|
362
|
+
</Button>
|
|
363
|
+
<Button type="text" disabled>
|
|
364
|
+
禁用
|
|
365
|
+
</Button>
|
|
366
|
+
<Button type="danger" disabled>
|
|
367
|
+
禁用
|
|
368
|
+
</Button>
|
|
369
|
+
</div>
|
|
370
|
+
),
|
|
371
|
+
parameters: {
|
|
372
|
+
docs: {
|
|
373
|
+
description: {
|
|
374
|
+
story: '展示不同变体的禁用状态。',
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Button 组件 - 简化版
|
|
2
|
+
|
|
3
|
+
一个精简的按钮组件,仅保留基本功能,专为简单项目使用而设计。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🎨 **6种变体**: primary, secondary, outline, ghost, link, danger
|
|
8
|
+
- 📏 **5种尺寸**: xs, sm, md, lg, xl
|
|
9
|
+
- 🔄 **圆角设置**: 支持方框和圆角两种样式
|
|
10
|
+
- 🔄 **加载状态**: 内置加载动画
|
|
11
|
+
- 🎯 **简洁设计**: 移除了复杂功能,专注于核心需求
|
|
12
|
+
- 📱 **响应式**: 支持基础响应式布局
|
|
13
|
+
- ♿ **无障碍**: 完整的键盘导航支持
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { Button } from "@/ui/Button";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 基础用法
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// 基础按钮
|
|
25
|
+
<Button>点击我</Button>
|
|
26
|
+
|
|
27
|
+
// 不同变体
|
|
28
|
+
<Button type="primary">主要按钮</Button>
|
|
29
|
+
<Button type="secondary">次要按钮</Button>
|
|
30
|
+
<Button type="outline">轮廓按钮</Button>
|
|
31
|
+
<Button type="ghost">幽灵按钮</Button>
|
|
32
|
+
<Button type="link">链接按钮</Button>
|
|
33
|
+
<Button type="danger">危险按钮</Button>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 尺寸
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<Button size="xs">超小按钮</Button>
|
|
40
|
+
<Button size="sm">小按钮</Button>
|
|
41
|
+
<Button size="md">中等按钮</Button>
|
|
42
|
+
<Button size="lg">大按钮</Button>
|
|
43
|
+
<Button size="xl">超大按钮</Button>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 圆角设置
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<Button rounded={false}>方框按钮</Button>
|
|
50
|
+
<Button rounded={true}>圆角按钮</Button>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 状态
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<Button loading>加载中</Button>
|
|
57
|
+
<Button disabled>禁用</Button>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 事件处理
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<Button
|
|
64
|
+
onClick={(e) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
console.log("按钮被点击");
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
点击我
|
|
70
|
+
</Button>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 表单集成
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<form>
|
|
77
|
+
<Button type="submit" variant="primary">
|
|
78
|
+
提交表单
|
|
79
|
+
</Button>
|
|
80
|
+
<Button type="reset" variant="secondary">
|
|
81
|
+
重置
|
|
82
|
+
</Button>
|
|
83
|
+
</form>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API 参考
|
|
87
|
+
|
|
88
|
+
### ButtonProps
|
|
89
|
+
|
|
90
|
+
| 属性 | 类型 | 默认值 | 描述 |
|
|
91
|
+
| --------- | --------------------------------- | ----------- | ---------------- |
|
|
92
|
+
| type | `ButtonType` | `'primary'` | 按钮变体 |
|
|
93
|
+
| size | `ButtonSize` | `'md'` | 按钮尺寸 |
|
|
94
|
+
| rounded | `boolean` | `false` | 是否圆角 |
|
|
95
|
+
| disabled | `boolean` | `false` | 是否禁用 |
|
|
96
|
+
| loading | `boolean` | `false` | 是否显示加载状态 |
|
|
97
|
+
| className | `string` | - | 自定义类名 |
|
|
98
|
+
| children | `ReactNode` | - | 按钮内容 |
|
|
99
|
+
| htmlType | `'button' \| 'submit' \| 'reset'` | `'button'` | HTML按钮类型 |
|
|
100
|
+
| onClick | `(event: MouseEvent) => void` | - | 点击事件处理器 |
|
|
101
|
+
|
|
102
|
+
### 类型定义
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
type ButtonType =
|
|
106
|
+
| "primary"
|
|
107
|
+
| "secondary"
|
|
108
|
+
| "outline"
|
|
109
|
+
| "ghost"
|
|
110
|
+
| "link"
|
|
111
|
+
| "danger";
|
|
112
|
+
type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 简化说明
|
|
116
|
+
|
|
117
|
+
相比完整版Button组件,简化版移除了以下功能:
|
|
118
|
+
|
|
119
|
+
- ❌ 复杂的形状选项(pill, square等)
|
|
120
|
+
- ❌ 多种状态类型(success, error等)
|
|
121
|
+
- ❌ 图标支持(leftIcon, rightIcon)
|
|
122
|
+
- ❌ 全宽支持
|
|
123
|
+
- ❌ 复杂的主题系统
|
|
124
|
+
|
|
125
|
+
## 最佳实践
|
|
126
|
+
|
|
127
|
+
1. **保持简洁**: 使用默认的primary变体作为主要操作
|
|
128
|
+
2. **尺寸一致**: 在同一界面中保持按钮尺寸的一致性
|
|
129
|
+
3. **状态反馈**: 使用loading状态提供用户反馈
|
|
130
|
+
4. **无障碍性**: 确保按钮有适当的标签和键盘导航
|
|
131
|
+
|
|
132
|
+
## 常见问题
|
|
133
|
+
|
|
134
|
+
### Q: 如何自定义按钮颜色?
|
|
135
|
+
|
|
136
|
+
A: 可以通过`className`属性添加自定义样式,或者修改`styles.ts`中的样式配置。
|
|
137
|
+
|
|
138
|
+
### Q: 按钮支持哪些事件?
|
|
139
|
+
|
|
140
|
+
A: 支持所有标准的HTML button元素事件,如onClick、onMouseOver等。
|
|
141
|
+
|
|
142
|
+
### Q: 如何实现按钮组?
|
|
143
|
+
|
|
144
|
+
A: 可以将多个Button组件包装在一个容器中,使用flex布局实现按钮组。
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
import type { ButtonProps, ButtonRef } from './types'
|
|
3
|
+
import { getButtonStyles } from './styles'
|
|
4
|
+
import { cn } from '@/utils'
|
|
5
|
+
import { LoadingIcon } from '@/icons'
|
|
6
|
+
|
|
7
|
+
export const Button = forwardRef<ButtonRef, ButtonProps>(
|
|
8
|
+
(
|
|
9
|
+
{
|
|
10
|
+
type = 'primary',
|
|
11
|
+
size = 'md',
|
|
12
|
+
rounded = false,
|
|
13
|
+
disabled = false,
|
|
14
|
+
loading = false,
|
|
15
|
+
className,
|
|
16
|
+
children,
|
|
17
|
+
htmlType = 'button',
|
|
18
|
+
onClick,
|
|
19
|
+
...props
|
|
20
|
+
},
|
|
21
|
+
ref
|
|
22
|
+
) => {
|
|
23
|
+
// 计算最终状态
|
|
24
|
+
const isDisabled = disabled || loading
|
|
25
|
+
|
|
26
|
+
// 获取样式类名
|
|
27
|
+
const buttonStyles = getButtonStyles(
|
|
28
|
+
type,
|
|
29
|
+
size,
|
|
30
|
+
rounded,
|
|
31
|
+
isDisabled,
|
|
32
|
+
loading,
|
|
33
|
+
className
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// 处理点击事件
|
|
37
|
+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
38
|
+
if (isDisabled) {
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
onClick?.(event)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
ref={ref}
|
|
48
|
+
type={htmlType}
|
|
49
|
+
disabled={isDisabled}
|
|
50
|
+
onClick={handleClick}
|
|
51
|
+
className={cn(buttonStyles)}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
{loading && <LoadingIcon className="animate-spin w-4 h-4 mr-2" />}
|
|
55
|
+
{children}
|
|
56
|
+
</button>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
export default Button
|
|
62
|
+
|
|
63
|
+
// 导出类型
|
|
64
|
+
export type {
|
|
65
|
+
ButtonProps,
|
|
66
|
+
ButtonRef,
|
|
67
|
+
ButtonType,
|
|
68
|
+
ButtonSize,
|
|
69
|
+
} from './types'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ButtonType, ButtonSize } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 获取按钮变体样式
|
|
5
|
+
*/
|
|
6
|
+
export const getVariantStyles = (variant: ButtonType): string => {
|
|
7
|
+
const variants = {
|
|
8
|
+
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
|
|
9
|
+
default: 'bg-white text-gray-700 hover:bg-gray-50 focus:ring-gray-500 border border-gray-300',
|
|
10
|
+
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500 border border-gray-300',
|
|
11
|
+
outline: 'bg-transparent text-blue-600 hover:bg-blue-50 focus:ring-blue-500 border border-blue-600',
|
|
12
|
+
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
|
|
13
|
+
text: 'bg-transparent text-gray-700 focus:ring-gray-500',
|
|
14
|
+
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
|
|
15
|
+
}
|
|
16
|
+
return variants[variant] || variants.primary
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 获取按钮尺寸样式
|
|
21
|
+
*/
|
|
22
|
+
export const getSizeStyles = (size: ButtonSize): string => {
|
|
23
|
+
const sizes = {
|
|
24
|
+
xs: 'px-2 py-1 text-xs min-h-[1.5rem]',
|
|
25
|
+
sm: 'px-3 py-1.5 text-sm min-h-[2rem]',
|
|
26
|
+
md: 'px-4 py-2 text-sm min-h-[2.5rem]',
|
|
27
|
+
lg: 'px-6 py-3 text-base min-h-[3rem]',
|
|
28
|
+
xl: 'px-8 py-4 text-lg min-h-[3.5rem]',
|
|
29
|
+
}
|
|
30
|
+
return sizes[size] || sizes.md
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 获取按钮圆角样式
|
|
35
|
+
*/
|
|
36
|
+
export const getRoundedStyles = (rounded: boolean): string => {
|
|
37
|
+
return rounded ? 'rounded-lg' : 'rounded-none'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 组合所有样式类名
|
|
42
|
+
*/
|
|
43
|
+
export const getButtonStyles = (
|
|
44
|
+
variant: ButtonType = 'primary',
|
|
45
|
+
size: ButtonSize = 'md',
|
|
46
|
+
rounded: boolean = false,
|
|
47
|
+
disabled: boolean = false,
|
|
48
|
+
loading: boolean = false,
|
|
49
|
+
customClassName?: string
|
|
50
|
+
): string => {
|
|
51
|
+
const baseStyles = 'inline-flex items-center justify-center font-medium text-center transition-all duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'
|
|
52
|
+
const variantStyles = getVariantStyles(variant)
|
|
53
|
+
const sizeStyles = getSizeStyles(size)
|
|
54
|
+
const roundedStyles = getRoundedStyles(rounded)
|
|
55
|
+
const disabledStyles = disabled ? 'opacity-50 cursor-not-allowed' : ''
|
|
56
|
+
const loadingStyles = loading ? 'cursor-wait' : ''
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
baseStyles,
|
|
60
|
+
variantStyles,
|
|
61
|
+
sizeStyles,
|
|
62
|
+
roundedStyles,
|
|
63
|
+
disabledStyles,
|
|
64
|
+
loadingStyles,
|
|
65
|
+
customClassName,
|
|
66
|
+
]
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join(' ')
|
|
69
|
+
.replace(/\s+/g, ' ')
|
|
70
|
+
.trim()
|
|
71
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReactNode, ButtonHTMLAttributes } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Button组件的变体类型 - 简化版
|
|
5
|
+
*/
|
|
6
|
+
export type ButtonType =
|
|
7
|
+
| 'primary' // 主要按钮
|
|
8
|
+
| 'default' // 默认按钮
|
|
9
|
+
| 'secondary' // 次要按钮
|
|
10
|
+
| 'outline' // 轮廓按钮
|
|
11
|
+
| 'ghost' // 幽灵按钮
|
|
12
|
+
| 'text' // 文本按钮
|
|
13
|
+
| 'danger' // 危险按钮
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Button组件的尺寸类型 - 简化版
|
|
17
|
+
*/
|
|
18
|
+
export type ButtonSize =
|
|
19
|
+
| 'xs' // 超小尺寸
|
|
20
|
+
| 'sm' // 小尺寸
|
|
21
|
+
| 'md' // 中等尺寸
|
|
22
|
+
| 'lg' // 大尺寸
|
|
23
|
+
| 'xl' // 超大尺寸
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Button组件的基础属性接口 - 简化版
|
|
28
|
+
*/
|
|
29
|
+
export interface ButtonProps
|
|
30
|
+
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'size' | 'children' | 'type'> {
|
|
31
|
+
/** 按钮变体 */
|
|
32
|
+
type?: ButtonType
|
|
33
|
+
/** 按钮尺寸 */
|
|
34
|
+
size?: ButtonSize
|
|
35
|
+
/** 是否圆角 */
|
|
36
|
+
rounded?: boolean
|
|
37
|
+
/** 是否禁用 */
|
|
38
|
+
disabled?: boolean
|
|
39
|
+
/** 是否显示加载状态 */
|
|
40
|
+
loading?: boolean
|
|
41
|
+
/** 自定义类名 */
|
|
42
|
+
className?: string
|
|
43
|
+
/** 子元素 */
|
|
44
|
+
children?: ReactNode
|
|
45
|
+
/** 按钮类型 */
|
|
46
|
+
htmlType?: ButtonHTMLAttributes<HTMLButtonElement>['type']
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Button组件的ref类型
|
|
51
|
+
*/
|
|
52
|
+
export type ButtonRef = HTMLButtonElement
|
package/src/utils/cn.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as cn } from './cn'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Path Aliases */
|
|
20
|
+
"baseUrl": ".",
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/* Linting */
|
|
26
|
+
"strict": true,
|
|
27
|
+
"noUnusedLocals": true,
|
|
28
|
+
"noUnusedParameters": true,
|
|
29
|
+
"erasableSyntaxOnly": true,
|
|
30
|
+
"noFallthroughCasesInSwitch": true,
|
|
31
|
+
"noUncheckedSideEffectImports": true
|
|
32
|
+
},
|
|
33
|
+
"include": ["src"]
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noEmit": false,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"emitDeclarationOnly": true,
|
|
13
|
+
"baseUrl": ".",
|
|
14
|
+
"paths": {
|
|
15
|
+
"@/*": ["./src/*"]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/libs", "src/utils", "src/icons"]
|
|
19
|
+
}
|
|
20
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/// <reference types="vitest/config" />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import dts from 'vite-plugin-dts';
|
|
5
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
|
11
|
+
import { playwright } from '@vitest/browser-playwright';
|
|
12
|
+
|
|
13
|
+
const dirname =
|
|
14
|
+
typeof __dirname !== 'undefined'
|
|
15
|
+
? __dirname
|
|
16
|
+
: path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
export default defineConfig(({ command }) => {
|
|
19
|
+
// 在构建时禁用 React Compiler,避免运行时注入 require 导致的 ES 模块兼容性问题
|
|
20
|
+
// React Compiler 应该在最终使用库的应用中运行
|
|
21
|
+
const enableReactCompiler = command !== 'build';
|
|
22
|
+
const isLibBuild = process.env.BUILD_LIB === 'true';
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
plugins: [
|
|
26
|
+
react({
|
|
27
|
+
babel: {
|
|
28
|
+
plugins: enableReactCompiler ? [['babel-plugin-react-compiler']] : [],
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
// ✅ 只在 dev / storybook 启用
|
|
33
|
+
!isLibBuild && tailwindcss(),
|
|
34
|
+
|
|
35
|
+
dts({
|
|
36
|
+
entryRoot: './src/libs',
|
|
37
|
+
tsconfigPath: './tsconfig.build.json',
|
|
38
|
+
insertTypesEntry: true,
|
|
39
|
+
rollupTypes: true,
|
|
40
|
+
exclude: ['**/*.css', '**/style-entry.ts', '**/*.stories.tsx', '**/*.test.ts', '**/*.test.tsx'],
|
|
41
|
+
}),
|
|
42
|
+
].filter(Boolean),
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
resolve: {
|
|
46
|
+
alias: {
|
|
47
|
+
'@': path.resolve(dirname, './src'),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
build: {
|
|
52
|
+
lib: {
|
|
53
|
+
entry: {
|
|
54
|
+
index: path.resolve(dirname, './src/libs/index.ts')
|
|
55
|
+
},
|
|
56
|
+
name: 'gemini-uis',
|
|
57
|
+
formats: ['es'],
|
|
58
|
+
fileName: () => 'index.js',
|
|
59
|
+
},
|
|
60
|
+
rollupOptions: {
|
|
61
|
+
external: [
|
|
62
|
+
'react',
|
|
63
|
+
'react-dom',
|
|
64
|
+
'react/jsx-runtime',
|
|
65
|
+
'react/jsx-dev-runtime',
|
|
66
|
+
],
|
|
67
|
+
output: {
|
|
68
|
+
assetFileNames: (assetInfo) => {
|
|
69
|
+
// 将所有 CSS 文件命名为 style.css
|
|
70
|
+
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
|
|
71
|
+
return 'style.css';
|
|
72
|
+
}
|
|
73
|
+
return assetInfo.name || 'assets/[name]-[hash].[ext]';
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
cssCodeSplit: false,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
test: {
|
|
81
|
+
projects: [
|
|
82
|
+
{
|
|
83
|
+
extends: true,
|
|
84
|
+
plugins: [
|
|
85
|
+
storybookTest({
|
|
86
|
+
configDir: path.join(dirname, '.storybook'),
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
test: {
|
|
90
|
+
name: 'storybook',
|
|
91
|
+
browser: {
|
|
92
|
+
enabled: true,
|
|
93
|
+
headless: true,
|
|
94
|
+
provider: playwright({}),
|
|
95
|
+
instances: [{ browser: 'chromium' }],
|
|
96
|
+
},
|
|
97
|
+
setupFiles: ['.storybook/vitest.setup.ts'],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="@vitest/browser-playwright" />
|