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.
@@ -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
@@ -0,0 +1,11 @@
1
+ # Gemini UIs 工作流程
2
+
3
+ Node 22, pnpm
4
+
5
+ pnpm install
6
+
7
+ pnpm dev
8
+
9
+ 调试代码后若有组件库修改,需要执行 pnpm changeset
10
+
11
+ 项目采用了代码检查,请注意代码规范
@@ -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
+ }
@@ -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,2 @@
1
+ export { default as LoadingIcon } from './LoadingIcon'
2
+ export type { IconProps } from './types'
@@ -0,0 +1,6 @@
1
+ import type { SVGProps } from 'react';
2
+
3
+ export interface IconProps extends SVGProps<SVGSVGElement> {
4
+ size?: number | string;
5
+ className?: string;
6
+ }
@@ -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
@@ -0,0 +1,10 @@
1
+ import './styles.css'
2
+ // 导出组件
3
+ export { default as Button } from './Button'
4
+ export type {
5
+ ButtonProps,
6
+ ButtonRef,
7
+ ButtonType,
8
+ ButtonSize,
9
+ } from './Button'
10
+
@@ -0,0 +1,2 @@
1
+ @import "tailwindcss";
2
+
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 合并 CSS 类名
3
+ */
4
+ function cn(...classes: (string | undefined | null | false)[]): string {
5
+ return classes.filter(Boolean).join(' ')
6
+ }
7
+
8
+ export default cn
@@ -0,0 +1 @@
1
+ export { default as cn } from './cn'
@@ -0,0 +1,7 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.css' {
4
+ const content: string
5
+ export default content
6
+ }
7
+
@@ -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,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -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" />