@zdyumath/react-ui 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/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # @your-scope/react-ui
2
+
3
+ 生产级 React 组件库模板,基于 Vite + TypeScript + CSS Modules。
4
+
5
+ [![CI](https://github.com/your-username/react-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/your-username/react-ui/actions)
6
+ [![npm version](https://img.shields.io/npm/v/@your-scope/react-ui)](https://www.npmjs.com/package/@your-scope/react-ui)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ npm install @your-scope/react-ui
13
+ # 或
14
+ pnpm add @your-scope/react-ui
15
+ # 或
16
+ yarn add @your-scope/react-ui
17
+ ```
18
+
19
+ **前置依赖(peerDependencies):** React ≥ 17
20
+
21
+ ## 使用
22
+
23
+ ```tsx
24
+ import { Button, Input } from '@your-scope/react-ui'
25
+ import '@your-scope/react-ui/styles' // 引入全局样式
26
+
27
+ function App() {
28
+ return (
29
+ <>
30
+ <Button variant="primary" onClick={() => alert('clicked')}>
31
+ 点击我
32
+ </Button>
33
+
34
+ <Input
35
+ label="邮箱"
36
+ required
37
+ placeholder="user@example.com"
38
+ helperText="我们不会分享你的邮箱"
39
+ />
40
+ </>
41
+ )
42
+ }
43
+ ```
44
+
45
+ ## 组件文档
46
+
47
+ ### Button
48
+
49
+ | Prop | 类型 | 默认值 | 说明 |
50
+ | ---------- | ------------------------------------------------- | ----------- | ------------------------ |
51
+ | variant | `primary \| secondary \| outline \| ghost \| danger` | `primary` | 按钮风格变体 |
52
+ | size | `sm \| md \| lg` | `md` | 按钮尺寸 |
53
+ | loading | `boolean` | `false` | 加载状态(自动禁用) |
54
+ | fullWidth | `boolean` | `false` | 是否撑满父容器 |
55
+ | leftIcon | `ReactNode` | — | 左侧图标 |
56
+ | rightIcon | `ReactNode` | — | 右侧图标 |
57
+ | ...rest | `ButtonHTMLAttributes` | — | 透传给 `<button>` 的属性 |
58
+
59
+ ### Input
60
+
61
+ | Prop | 类型 | 默认值 | 说明 |
62
+ | ------------ | ------------------- | ------- | ------------------------------------ |
63
+ | size | `sm \| md \| lg` | `md` | 输入框尺寸 |
64
+ | label | `string` | — | 标签文字 |
65
+ | required | `boolean` | `false` | 是否必填 |
66
+ | errorMessage | `string` | — | 错误提示(传入后变为错误状态) |
67
+ | helperText | `string` | — | 帮助提示(与 errorMessage 互斥) |
68
+ | prefixIcon | `ReactNode` | — | 前置图标 |
69
+ | suffixIcon | `ReactNode` | — | 后置图标 |
70
+ | ...rest | `InputHTMLAttributes` | — | 透传给 `<input>` 的属性 |
71
+
72
+ ## 开发
73
+
74
+ ```bash
75
+ # 安装依赖
76
+ npm install
77
+
78
+ # 启动 Storybook 开发环境
79
+ npm run storybook
80
+
81
+ # 运行单元测试(watch 模式)
82
+ npm run test:watch
83
+
84
+ # 构建产物
85
+ npm run build
86
+
87
+ # 代码检查
88
+ npm run lint
89
+ ```
90
+
91
+ ## 发布到 npm
92
+
93
+ ```bash
94
+ # 1. 更新 package.json 中的 version
95
+ npm version patch # 0.1.0 → 0.1.1
96
+ npm version minor # 0.1.0 → 0.2.0
97
+ npm version major # 0.1.0 → 1.0.0
98
+
99
+ # 2. 推送 tag,GitHub Actions 自动发布
100
+ git push --follow-tags
101
+
102
+ # 3. 或手动发布
103
+ npm publish --access public
104
+ ```
105
+
106
+ ## 项目结构
107
+
108
+ ```
109
+ .
110
+ ├── src/
111
+ │ ├── components/
112
+ │ │ ├── Button/ # 按钮组件
113
+ │ │ │ ├── Button.tsx
114
+ │ │ │ ├── Button.module.css
115
+ │ │ │ ├── Button.test.tsx
116
+ │ │ │ ├── Button.stories.tsx
117
+ │ │ │ └── index.ts
118
+ │ │ ├── Input/ # 输入框组件(结构同上)
119
+ │ │ └── index.ts # 统一导出入口
120
+ │ ├── index.ts # 库入口
121
+ │ └── test-setup.ts # 测试环境初始化
122
+ ├── .storybook/ # Storybook 配置
123
+ ├── .github/workflows/ # CI/CD 流水线
124
+ ├── dist/ # 构建产物(不提交 Git)
125
+ ├── vite.config.ts
126
+ ├── tsconfig.json
127
+ └── package.json
128
+ ```
129
+
130
+ ## License
131
+
132
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react/jsx-runtime"),x=require("react"),b="_btn_1wxjz_2",k="_loading_1wxjz_97",N="_fullWidth_1wxjz_104",y="_spinner_1wxjz_109",I="_spin_1wxjz_109",n={btn:b,"size-sm":"_size-sm_1wxjz_27","size-md":"_size-md_1wxjz_34","size-lg":"_size-lg_1wxjz_38","variant-primary":"_variant-primary_1wxjz_46","variant-secondary":"_variant-secondary_1wxjz_59","variant-outline":"_variant-outline_1wxjz_68","variant-ghost":"_variant-ghost_1wxjz_77","variant-danger":"_variant-danger_1wxjz_86",loading:k,fullWidth:N,spinner:y,spin:I},j=x.forwardRef(({variant:c="primary",size:l="md",loading:a=!1,fullWidth:e=!1,leftIcon:r,rightIcon:t,children:_,className:d,disabled:p,...u},m)=>{const h=p||a,o=[n.btn,n[`size-${l}`],n[`variant-${c}`],a?n.loading:"",e?n.fullWidth:"",d??""].filter(Boolean).join(" ");return s.jsxs("button",{ref:m,className:o,disabled:h,"aria-busy":a,...u,children:[a?s.jsx("span",{className:n.spinner,"aria-hidden":"true"}):r,_,!a&&t]})});j.displayName="Button";const W="_wrapper_1mihk_2",g="_label_1mihk_10",B="_required_1mihk_17",q="_inputWrapper_1mihk_23",S="_input_1mihk_23",P="_error_1mihk_66",$="_prefixIcon_1mihk_81",R="_suffixIcon_1mihk_90",T="_hasPrefix_1mihk_100",C="_hasSuffix_1mihk_104",D="_helperText_1mihk_109",F="_errorMessage_1mihk_115",i={wrapper:W,label:g,required:B,inputWrapper:q,input:S,"size-sm":"_size-sm_1mihk_53","size-md":"_size-md_1mihk_57","size-lg":"_size-lg_1mihk_60",error:P,prefixIcon:$,suffixIcon:R,hasPrefix:T,hasSuffix:C,helperText:D,errorMessage:F},z=x.forwardRef(({size:c="md",label:l,required:a,errorMessage:e,helperText:r,prefixIcon:t,suffixIcon:_,className:d,id:p,...u},m)=>{const h=x.useId(),o=p??h,f=`${o}-desc`,w=[i.wrapper,i[`size-${c}`],e?i.error:""].filter(Boolean).join(" "),v=[i.inputWrapper,t?i.hasPrefix:"",_?i.hasSuffix:""].filter(Boolean).join(" ");return s.jsxs("div",{className:w,children:[l&&s.jsxs("label",{className:i.label,htmlFor:o,children:[l,a&&s.jsx("span",{className:i.required,"aria-hidden":"true",children:"*"})]}),s.jsxs("div",{className:v,children:[t&&s.jsx("span",{className:i.prefixIcon,"aria-hidden":"true",children:t}),s.jsx("input",{ref:m,id:o,className:[i.input,d??""].filter(Boolean).join(" "),"aria-required":a,"aria-invalid":!!e,"aria-describedby":e||r?f:void 0,...u}),_&&s.jsx("span",{className:i.suffixIcon,"aria-hidden":"true",children:_})]}),(e||r)&&s.jsx("span",{id:f,className:e?i.errorMessage:i.helperText,role:e?"alert":void 0,children:e??r})]})});z.displayName="Input";exports.Button=j;exports.Input=z;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/components/Button/Button.tsx","../src/components/Input/Input.tsx"],"sourcesContent":["import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react'\nimport styles from './Button.module.css'\n\n// 组件支持的尺寸:sm(小) / md(中,默认) / lg(大)\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n// 组件支持的风格变体\nexport type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** 按钮风格变体,默认 primary */\n variant?: ButtonVariant\n /** 按钮尺寸,默认 md */\n size?: ButtonSize\n /** 是否处于加载状态 */\n loading?: boolean\n /** 是否撑满父容器宽度 */\n fullWidth?: boolean\n /** 左侧图标(任意 ReactNode,建议使用 SVG 图标) */\n leftIcon?: ReactNode\n /** 右侧图标 */\n rightIcon?: ReactNode\n}\n\n/**\n * Button 组件\n *\n * 支持 5 种视觉风格 × 3 种尺寸 × loading/disabled 状态。\n * 使用 forwardRef 以便父组件可以获取底层 <button> DOM 引用。\n *\n * @example\n * <Button variant=\"primary\" size=\"md\" onClick={handleClick}>\n * 提交\n * </Button>\n *\n * @example 加载状态\n * <Button loading leftIcon={<SaveIcon />}>保存</Button>\n */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n variant = 'primary',\n size = 'md',\n loading = false,\n fullWidth = false,\n leftIcon,\n rightIcon,\n children,\n className,\n disabled,\n ...rest\n },\n ref,\n ) => {\n const isDisabled = disabled || loading\n\n // 拼接 CSS Modules 类名\n const classNames = [\n styles.btn,\n styles[`size-${size}`],\n styles[`variant-${variant}`],\n loading ? styles.loading : '',\n fullWidth ? styles.fullWidth : '',\n className ?? '',\n ]\n .filter(Boolean)\n .join(' ')\n\n return (\n <button ref={ref} className={classNames} disabled={isDisabled} aria-busy={loading} {...rest}>\n {/* 加载中时用 spinner 替换左侧图标 */}\n {loading ? <span className={styles.spinner} aria-hidden=\"true\" /> : leftIcon}\n {children}\n {!loading && rightIcon}\n </button>\n )\n },\n)\n\nButton.displayName = 'Button'\n","import { InputHTMLAttributes, ReactNode, forwardRef, useId } from 'react'\nimport styles from './Input.module.css'\n\nexport type InputSize = 'sm' | 'md' | 'lg'\n\nexport interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {\n /** 输入框尺寸,默认 md */\n size?: InputSize\n /** 显示在上方的标签文字 */\n label?: string\n /** 是否必填(在 label 后显示红色 *) */\n required?: boolean\n /** 错误提示信息(传入后输入框变为错误状态) */\n errorMessage?: string\n /** 帮助提示文字(与 errorMessage 互斥,优先展示 errorMessage) */\n helperText?: string\n /** 前置图标(建议 16×16 SVG) */\n prefixIcon?: ReactNode\n /** 后置图标(建议 16×16 SVG) */\n suffixIcon?: ReactNode\n}\n\n/**\n * Input 组件\n *\n * 包含 label、帮助文本、错误状态、前后置图标,并与 htmlFor/aria-describedby 无障碍关联。\n * 使用 forwardRef 以便父组件可以访问底层 <input> DOM。\n *\n * @example\n * <Input\n * label=\"用户名\"\n * required\n * placeholder=\"请输入用户名\"\n * errorMessage={errors.username?.message}\n * />\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(\n (\n {\n size = 'md',\n label,\n required,\n errorMessage,\n helperText,\n prefixIcon,\n suffixIcon,\n className,\n id: propId,\n ...rest\n },\n ref,\n ) => {\n // 自动生成唯一 id,保证 label 与 input 的无障碍关联\n const autoId = useId()\n const id = propId ?? autoId\n const descId = `${id}-desc`\n\n const wrapperClassName = [\n styles.wrapper,\n styles[`size-${size}`],\n errorMessage ? styles.error : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n const inputWrapperClassName = [\n styles.inputWrapper,\n prefixIcon ? styles.hasPrefix : '',\n suffixIcon ? styles.hasSuffix : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n return (\n <div className={wrapperClassName}>\n {/* 标签区域 */}\n {label && (\n <label className={styles.label} htmlFor={id}>\n {label}\n {required && (\n <span className={styles.required} aria-hidden=\"true\">\n *\n </span>\n )}\n </label>\n )}\n\n {/* 输入框 + 图标 */}\n <div className={inputWrapperClassName}>\n {prefixIcon && (\n <span className={styles.prefixIcon} aria-hidden=\"true\">\n {prefixIcon}\n </span>\n )}\n\n <input\n ref={ref}\n id={id}\n className={[styles.input, className ?? ''].filter(Boolean).join(' ')}\n aria-required={required}\n aria-invalid={!!errorMessage}\n aria-describedby={errorMessage || helperText ? descId : undefined}\n {...rest}\n />\n\n {suffixIcon && (\n <span className={styles.suffixIcon} aria-hidden=\"true\">\n {suffixIcon}\n </span>\n )}\n </div>\n\n {/* 错误/帮助文字,优先展示错误信息 */}\n {(errorMessage || helperText) && (\n <span\n id={descId}\n className={errorMessage ? styles.errorMessage : styles.helperText}\n role={errorMessage ? 'alert' : undefined}\n >\n {errorMessage ?? helperText}\n </span>\n )}\n </div>\n )\n },\n)\n\nInput.displayName = 'Input'\n"],"names":["Button","forwardRef","variant","size","loading","fullWidth","leftIcon","rightIcon","children","className","disabled","rest","ref","isDisabled","classNames","styles","jsxs","Input","label","required","errorMessage","helperText","prefixIcon","suffixIcon","propId","autoId","useId","id","descId","wrapperClassName","inputWrapperClassName","jsx"],"mappings":"kmBAsCaA,EAASC,EAAAA,WACpB,CACE,CACE,QAAAC,EAAU,UACV,KAAAC,EAAO,KACP,QAAAC,EAAU,GACV,UAAAC,EAAY,GACZ,SAAAC,EACA,UAAAC,EACA,SAAAC,EACA,UAAAC,EACA,SAAAC,EACA,GAAGC,CAAA,EAELC,IACG,CACH,MAAMC,EAAaH,GAAYN,EAGzBU,EAAa,CACjBC,EAAO,IACPA,EAAO,QAAQZ,CAAI,EAAE,EACrBY,EAAO,WAAWb,CAAO,EAAE,EAC3BE,EAAUW,EAAO,QAAU,GAC3BV,EAAYU,EAAO,UAAY,GAC/BN,GAAa,EAAA,EAEZ,OAAO,OAAO,EACd,KAAK,GAAG,EAEX,OACEO,EAAAA,KAAC,SAAA,CAAO,IAAAJ,EAAU,UAAWE,EAAY,SAAUD,EAAY,YAAWT,EAAU,GAAGO,EAEpF,SAAA,CAAAP,QAAW,OAAA,CAAK,UAAWW,EAAO,QAAS,cAAY,OAAO,EAAKT,EACnEE,EACA,CAACJ,GAAWG,CAAA,EACf,CAEJ,CACF,EAEAP,EAAO,YAAc,ohBC3CRiB,EAAQhB,EAAAA,WACnB,CACE,CACE,KAAAE,EAAO,KACP,MAAAe,EACA,SAAAC,EACA,aAAAC,EACA,WAAAC,EACA,WAAAC,EACA,WAAAC,EACA,UAAAd,EACA,GAAIe,EACJ,GAAGb,CAAA,EAELC,IACG,CAEH,MAAMa,EAASC,EAAAA,MAAA,EACTC,EAAKH,GAAUC,EACfG,EAAS,GAAGD,CAAE,QAEdE,EAAmB,CACvBd,EAAO,QACPA,EAAO,QAAQZ,CAAI,EAAE,EACrBiB,EAAeL,EAAO,MAAQ,EAAA,EAE7B,OAAO,OAAO,EACd,KAAK,GAAG,EAELe,EAAwB,CAC5Bf,EAAO,aACPO,EAAaP,EAAO,UAAY,GAChCQ,EAAaR,EAAO,UAAY,EAAA,EAE/B,OAAO,OAAO,EACd,KAAK,GAAG,EAEX,OACEC,EAAAA,KAAC,MAAA,CAAI,UAAWa,EAEb,SAAA,CAAAX,UACE,QAAA,CAAM,UAAWH,EAAO,MAAO,QAASY,EACtC,SAAA,CAAAT,EACAC,SACE,OAAA,CAAK,UAAWJ,EAAO,SAAU,cAAY,OAAO,SAAA,GAAA,CAErD,CAAA,EAEJ,EAIFC,EAAAA,KAAC,MAAA,CAAI,UAAWc,EACb,SAAA,CAAAR,SACE,OAAA,CAAK,UAAWP,EAAO,WAAY,cAAY,OAC7C,SAAAO,CAAA,CACH,EAGFS,EAAAA,IAAC,QAAA,CACC,IAAAnB,EACA,GAAAe,EACA,UAAW,CAACZ,EAAO,MAAON,GAAa,EAAE,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,EACnE,gBAAeU,EACf,eAAc,CAAC,CAACC,EAChB,mBAAkBA,GAAgBC,EAAaO,EAAS,OACvD,GAAGjB,CAAA,CAAA,EAGLY,SACE,OAAA,CAAK,UAAWR,EAAO,WAAY,cAAY,OAC7C,SAAAQ,CAAA,CACH,CAAA,EAEJ,GAGEH,GAAgBC,IAChBU,EAAAA,IAAC,OAAA,CACC,GAAIH,EACJ,UAAWR,EAAeL,EAAO,aAAeA,EAAO,WACvD,KAAMK,EAAe,QAAU,OAE9B,SAAAA,GAAgBC,CAAA,CAAA,CACnB,EAEJ,CAEJ,CACF,EAEAJ,EAAM,YAAc"}
@@ -0,0 +1,77 @@
1
+ import { ButtonHTMLAttributes } from 'react';
2
+ import { ForwardRefExoticComponent } from 'react';
3
+ import { InputHTMLAttributes } from 'react';
4
+ import { ReactNode } from 'react';
5
+ import { RefAttributes } from 'react';
6
+
7
+ /**
8
+ * Button 组件
9
+ *
10
+ * 支持 5 种视觉风格 × 3 种尺寸 × loading/disabled 状态。
11
+ * 使用 forwardRef 以便父组件可以获取底层 <button> DOM 引用。
12
+ *
13
+ * @example
14
+ * <Button variant="primary" size="md" onClick={handleClick}>
15
+ * 提交
16
+ * </Button>
17
+ *
18
+ * @example 加载状态
19
+ * <Button loading leftIcon={<SaveIcon />}>保存</Button>
20
+ */
21
+ export declare const Button: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>>;
22
+
23
+ export declare interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
24
+ /** 按钮风格变体,默认 primary */
25
+ variant?: ButtonVariant;
26
+ /** 按钮尺寸,默认 md */
27
+ size?: ButtonSize;
28
+ /** 是否处于加载状态 */
29
+ loading?: boolean;
30
+ /** 是否撑满父容器宽度 */
31
+ fullWidth?: boolean;
32
+ /** 左侧图标(任意 ReactNode,建议使用 SVG 图标) */
33
+ leftIcon?: ReactNode;
34
+ /** 右侧图标 */
35
+ rightIcon?: ReactNode;
36
+ }
37
+
38
+ export declare type ButtonSize = 'sm' | 'md' | 'lg';
39
+
40
+ export declare type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
41
+
42
+ /**
43
+ * Input 组件
44
+ *
45
+ * 包含 label、帮助文本、错误状态、前后置图标,并与 htmlFor/aria-describedby 无障碍关联。
46
+ * 使用 forwardRef 以便父组件可以访问底层 <input> DOM。
47
+ *
48
+ * @example
49
+ * <Input
50
+ * label="用户名"
51
+ * required
52
+ * placeholder="请输入用户名"
53
+ * errorMessage={errors.username?.message}
54
+ * />
55
+ */
56
+ export declare const Input: ForwardRefExoticComponent<InputProps & RefAttributes<HTMLInputElement>>;
57
+
58
+ export declare interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
59
+ /** 输入框尺寸,默认 md */
60
+ size?: InputSize;
61
+ /** 显示在上方的标签文字 */
62
+ label?: string;
63
+ /** 是否必填(在 label 后显示红色 *) */
64
+ required?: boolean;
65
+ /** 错误提示信息(传入后输入框变为错误状态) */
66
+ errorMessage?: string;
67
+ /** 帮助提示文字(与 errorMessage 互斥,优先展示 errorMessage) */
68
+ helperText?: string;
69
+ /** 前置图标(建议 16×16 SVG) */
70
+ prefixIcon?: ReactNode;
71
+ /** 后置图标(建议 16×16 SVG) */
72
+ suffixIcon?: ReactNode;
73
+ }
74
+
75
+ export declare type InputSize = 'sm' | 'md' | 'lg';
76
+
77
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,122 @@
1
+ import { jsxs as c, jsx as n } from "react/jsx-runtime";
2
+ import { forwardRef as z, useId as v } from "react";
3
+ const k = "_btn_1wxjz_2", N = "_loading_1wxjz_97", b = "_fullWidth_1wxjz_104", y = "_spinner_1wxjz_109", I = "_spin_1wxjz_109", e = {
4
+ btn: k,
5
+ "size-sm": "_size-sm_1wxjz_27",
6
+ "size-md": "_size-md_1wxjz_34",
7
+ "size-lg": "_size-lg_1wxjz_38",
8
+ "variant-primary": "_variant-primary_1wxjz_46",
9
+ "variant-secondary": "_variant-secondary_1wxjz_59",
10
+ "variant-outline": "_variant-outline_1wxjz_68",
11
+ "variant-ghost": "_variant-ghost_1wxjz_77",
12
+ "variant-danger": "_variant-danger_1wxjz_86",
13
+ loading: N,
14
+ fullWidth: b,
15
+ spinner: y,
16
+ spin: I
17
+ }, W = z(
18
+ ({
19
+ variant: p = "primary",
20
+ size: l = "md",
21
+ loading: a = !1,
22
+ fullWidth: s = !1,
23
+ leftIcon: r,
24
+ rightIcon: t,
25
+ children: _,
26
+ className: d,
27
+ disabled: m,
28
+ ...h
29
+ }, u) => {
30
+ const f = m || a, o = [
31
+ e.btn,
32
+ e[`size-${l}`],
33
+ e[`variant-${p}`],
34
+ a ? e.loading : "",
35
+ s ? e.fullWidth : "",
36
+ d ?? ""
37
+ ].filter(Boolean).join(" ");
38
+ return /* @__PURE__ */ c("button", { ref: u, className: o, disabled: f, "aria-busy": a, ...h, children: [
39
+ a ? /* @__PURE__ */ n("span", { className: e.spinner, "aria-hidden": "true" }) : r,
40
+ _,
41
+ !a && t
42
+ ] });
43
+ }
44
+ );
45
+ W.displayName = "Button";
46
+ const g = "_wrapper_1mihk_2", B = "_label_1mihk_10", $ = "_required_1mihk_17", q = "_inputWrapper_1mihk_23", P = "_input_1mihk_23", S = "_error_1mihk_66", C = "_prefixIcon_1mihk_81", T = "_suffixIcon_1mihk_90", D = "_hasPrefix_1mihk_100", F = "_hasSuffix_1mihk_104", R = "_helperText_1mihk_109", A = "_errorMessage_1mihk_115", i = {
47
+ wrapper: g,
48
+ label: B,
49
+ required: $,
50
+ inputWrapper: q,
51
+ input: P,
52
+ "size-sm": "_size-sm_1mihk_53",
53
+ "size-md": "_size-md_1mihk_57",
54
+ "size-lg": "_size-lg_1mihk_60",
55
+ error: S,
56
+ prefixIcon: C,
57
+ suffixIcon: T,
58
+ hasPrefix: D,
59
+ hasSuffix: F,
60
+ helperText: R,
61
+ errorMessage: A
62
+ }, E = z(
63
+ ({
64
+ size: p = "md",
65
+ label: l,
66
+ required: a,
67
+ errorMessage: s,
68
+ helperText: r,
69
+ prefixIcon: t,
70
+ suffixIcon: _,
71
+ className: d,
72
+ id: m,
73
+ ...h
74
+ }, u) => {
75
+ const f = v(), o = m ?? f, x = `${o}-desc`, j = [
76
+ i.wrapper,
77
+ i[`size-${p}`],
78
+ s ? i.error : ""
79
+ ].filter(Boolean).join(" "), w = [
80
+ i.inputWrapper,
81
+ t ? i.hasPrefix : "",
82
+ _ ? i.hasSuffix : ""
83
+ ].filter(Boolean).join(" ");
84
+ return /* @__PURE__ */ c("div", { className: j, children: [
85
+ l && /* @__PURE__ */ c("label", { className: i.label, htmlFor: o, children: [
86
+ l,
87
+ a && /* @__PURE__ */ n("span", { className: i.required, "aria-hidden": "true", children: "*" })
88
+ ] }),
89
+ /* @__PURE__ */ c("div", { className: w, children: [
90
+ t && /* @__PURE__ */ n("span", { className: i.prefixIcon, "aria-hidden": "true", children: t }),
91
+ /* @__PURE__ */ n(
92
+ "input",
93
+ {
94
+ ref: u,
95
+ id: o,
96
+ className: [i.input, d ?? ""].filter(Boolean).join(" "),
97
+ "aria-required": a,
98
+ "aria-invalid": !!s,
99
+ "aria-describedby": s || r ? x : void 0,
100
+ ...h
101
+ }
102
+ ),
103
+ _ && /* @__PURE__ */ n("span", { className: i.suffixIcon, "aria-hidden": "true", children: _ })
104
+ ] }),
105
+ (s || r) && /* @__PURE__ */ n(
106
+ "span",
107
+ {
108
+ id: x,
109
+ className: s ? i.errorMessage : i.helperText,
110
+ role: s ? "alert" : void 0,
111
+ children: s ?? r
112
+ }
113
+ )
114
+ ] });
115
+ }
116
+ );
117
+ E.displayName = "Input";
118
+ export {
119
+ W as Button,
120
+ E as Input
121
+ };
122
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/components/Button/Button.tsx","../src/components/Input/Input.tsx"],"sourcesContent":["import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react'\nimport styles from './Button.module.css'\n\n// 组件支持的尺寸:sm(小) / md(中,默认) / lg(大)\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n// 组件支持的风格变体\nexport type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** 按钮风格变体,默认 primary */\n variant?: ButtonVariant\n /** 按钮尺寸,默认 md */\n size?: ButtonSize\n /** 是否处于加载状态 */\n loading?: boolean\n /** 是否撑满父容器宽度 */\n fullWidth?: boolean\n /** 左侧图标(任意 ReactNode,建议使用 SVG 图标) */\n leftIcon?: ReactNode\n /** 右侧图标 */\n rightIcon?: ReactNode\n}\n\n/**\n * Button 组件\n *\n * 支持 5 种视觉风格 × 3 种尺寸 × loading/disabled 状态。\n * 使用 forwardRef 以便父组件可以获取底层 <button> DOM 引用。\n *\n * @example\n * <Button variant=\"primary\" size=\"md\" onClick={handleClick}>\n * 提交\n * </Button>\n *\n * @example 加载状态\n * <Button loading leftIcon={<SaveIcon />}>保存</Button>\n */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n variant = 'primary',\n size = 'md',\n loading = false,\n fullWidth = false,\n leftIcon,\n rightIcon,\n children,\n className,\n disabled,\n ...rest\n },\n ref,\n ) => {\n const isDisabled = disabled || loading\n\n // 拼接 CSS Modules 类名\n const classNames = [\n styles.btn,\n styles[`size-${size}`],\n styles[`variant-${variant}`],\n loading ? styles.loading : '',\n fullWidth ? styles.fullWidth : '',\n className ?? '',\n ]\n .filter(Boolean)\n .join(' ')\n\n return (\n <button ref={ref} className={classNames} disabled={isDisabled} aria-busy={loading} {...rest}>\n {/* 加载中时用 spinner 替换左侧图标 */}\n {loading ? <span className={styles.spinner} aria-hidden=\"true\" /> : leftIcon}\n {children}\n {!loading && rightIcon}\n </button>\n )\n },\n)\n\nButton.displayName = 'Button'\n","import { InputHTMLAttributes, ReactNode, forwardRef, useId } from 'react'\nimport styles from './Input.module.css'\n\nexport type InputSize = 'sm' | 'md' | 'lg'\n\nexport interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {\n /** 输入框尺寸,默认 md */\n size?: InputSize\n /** 显示在上方的标签文字 */\n label?: string\n /** 是否必填(在 label 后显示红色 *) */\n required?: boolean\n /** 错误提示信息(传入后输入框变为错误状态) */\n errorMessage?: string\n /** 帮助提示文字(与 errorMessage 互斥,优先展示 errorMessage) */\n helperText?: string\n /** 前置图标(建议 16×16 SVG) */\n prefixIcon?: ReactNode\n /** 后置图标(建议 16×16 SVG) */\n suffixIcon?: ReactNode\n}\n\n/**\n * Input 组件\n *\n * 包含 label、帮助文本、错误状态、前后置图标,并与 htmlFor/aria-describedby 无障碍关联。\n * 使用 forwardRef 以便父组件可以访问底层 <input> DOM。\n *\n * @example\n * <Input\n * label=\"用户名\"\n * required\n * placeholder=\"请输入用户名\"\n * errorMessage={errors.username?.message}\n * />\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(\n (\n {\n size = 'md',\n label,\n required,\n errorMessage,\n helperText,\n prefixIcon,\n suffixIcon,\n className,\n id: propId,\n ...rest\n },\n ref,\n ) => {\n // 自动生成唯一 id,保证 label 与 input 的无障碍关联\n const autoId = useId()\n const id = propId ?? autoId\n const descId = `${id}-desc`\n\n const wrapperClassName = [\n styles.wrapper,\n styles[`size-${size}`],\n errorMessage ? styles.error : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n const inputWrapperClassName = [\n styles.inputWrapper,\n prefixIcon ? styles.hasPrefix : '',\n suffixIcon ? styles.hasSuffix : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n return (\n <div className={wrapperClassName}>\n {/* 标签区域 */}\n {label && (\n <label className={styles.label} htmlFor={id}>\n {label}\n {required && (\n <span className={styles.required} aria-hidden=\"true\">\n *\n </span>\n )}\n </label>\n )}\n\n {/* 输入框 + 图标 */}\n <div className={inputWrapperClassName}>\n {prefixIcon && (\n <span className={styles.prefixIcon} aria-hidden=\"true\">\n {prefixIcon}\n </span>\n )}\n\n <input\n ref={ref}\n id={id}\n className={[styles.input, className ?? ''].filter(Boolean).join(' ')}\n aria-required={required}\n aria-invalid={!!errorMessage}\n aria-describedby={errorMessage || helperText ? descId : undefined}\n {...rest}\n />\n\n {suffixIcon && (\n <span className={styles.suffixIcon} aria-hidden=\"true\">\n {suffixIcon}\n </span>\n )}\n </div>\n\n {/* 错误/帮助文字,优先展示错误信息 */}\n {(errorMessage || helperText) && (\n <span\n id={descId}\n className={errorMessage ? styles.errorMessage : styles.helperText}\n role={errorMessage ? 'alert' : undefined}\n >\n {errorMessage ?? helperText}\n </span>\n )}\n </div>\n )\n },\n)\n\nInput.displayName = 'Input'\n"],"names":["Button","forwardRef","variant","size","loading","fullWidth","leftIcon","rightIcon","children","className","disabled","rest","ref","isDisabled","classNames","styles","jsxs","Input","label","required","errorMessage","helperText","prefixIcon","suffixIcon","propId","autoId","useId","id","descId","wrapperClassName","inputWrapperClassName","jsx"],"mappings":"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"}
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ ._btn_1wxjz_2{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:0 16px;border:1px solid transparent;border-radius:6px;font-size:14px;font-weight:500;line-height:1;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap;transition:background-color .2s,border-color .2s,box-shadow .2s,opacity .2s;outline:none;text-decoration:none;font-family:inherit}._btn_1wxjz_2:focus-visible{box-shadow:0 0 0 3px #3b82f666}._size-sm_1wxjz_27{height:28px;font-size:12px;padding:0 10px;border-radius:4px}._size-md_1wxjz_34{height:36px}._size-lg_1wxjz_38{height:44px;font-size:16px;padding:0 24px;border-radius:8px}._variant-primary_1wxjz_46{background-color:#3b82f6;border-color:#3b82f6;color:#fff}._variant-primary_1wxjz_46:hover:not(:disabled){background-color:#2563eb;border-color:#2563eb}._variant-primary_1wxjz_46:active:not(:disabled){background-color:#1d4ed8}._variant-secondary_1wxjz_59{background-color:#f3f4f6;border-color:#e5e7eb;color:#374151}._variant-secondary_1wxjz_59:hover:not(:disabled){background-color:#e5e7eb}._variant-outline_1wxjz_68{background-color:transparent;border-color:#3b82f6;color:#3b82f6}._variant-outline_1wxjz_68:hover:not(:disabled){background-color:#eff6ff}._variant-ghost_1wxjz_77{background-color:transparent;border-color:transparent;color:#374151}._variant-ghost_1wxjz_77:hover:not(:disabled){background-color:#f3f4f6}._variant-danger_1wxjz_86{background-color:#ef4444;border-color:#ef4444;color:#fff}._variant-danger_1wxjz_86:hover:not(:disabled){background-color:#dc2626}._btn_1wxjz_2:disabled,._loading_1wxjz_97{opacity:.6;cursor:not-allowed;pointer-events:none}._fullWidth_1wxjz_104{width:100%}._spinner_1wxjz_109{width:14px;height:14px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:_spin_1wxjz_109 .6s linear infinite;flex-shrink:0}@keyframes _spin_1wxjz_109{to{transform:rotate(360deg)}}._wrapper_1mihk_2{display:flex;flex-direction:column;gap:4px;width:100%}._label_1mihk_10{font-size:14px;font-weight:500;color:#374151;line-height:1.4}._required_1mihk_17{color:#ef4444;margin-left:2px}._inputWrapper_1mihk_23{position:relative;display:flex;align-items:center}._input_1mihk_23{width:100%;padding:0 12px;border:1px solid #d1d5db;border-radius:6px;font-size:14px;color:#111827;background-color:#fff;transition:border-color .2s,box-shadow .2s;outline:none;font-family:inherit}._input_1mihk_23::placeholder{color:#9ca3af}._input_1mihk_23:focus{border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f626}._size-sm_1mihk_53 ._input_1mihk_23{height:28px;font-size:12px}._size-md_1mihk_57 ._input_1mihk_23{height:36px}._size-lg_1mihk_60 ._input_1mihk_23{height:44px;font-size:16px}._error_1mihk_66 ._input_1mihk_23{border-color:#ef4444}._error_1mihk_66 ._input_1mihk_23:focus{box-shadow:0 0 0 3px #ef444426}._input_1mihk_23:disabled{background-color:#f9fafb;color:#9ca3af;cursor:not-allowed}._prefixIcon_1mihk_81{position:absolute;left:10px;display:flex;align-items:center;color:#9ca3af;pointer-events:none}._suffixIcon_1mihk_90{position:absolute;right:10px;display:flex;align-items:center;color:#9ca3af;pointer-events:none}._hasPrefix_1mihk_100 ._input_1mihk_23{padding-left:34px}._hasSuffix_1mihk_104 ._input_1mihk_23{padding-right:34px}._helperText_1mihk_109{font-size:12px;color:#6b7280;line-height:1.4}._errorMessage_1mihk_115{font-size:12px;color:#ef4444;line-height:1.4}
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@zdyumath/react-ui",
3
+ "version": "0.1.0",
4
+ "description": "生产级 React 组件库模板",
5
+ "author": "zdyumath <zdyumath@gmail.com>",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "react",
9
+ "components",
10
+ "ui",
11
+ "typescript"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/zdyumath/react-ui"
16
+ },
17
+ "type": "module",
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "import": "./dist/index.js",
24
+ "require": "./dist/index.cjs",
25
+ "types": "./dist/index.d.ts"
26
+ },
27
+ "./styles": "./dist/style.css"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "CHANGELOG.md"
33
+ ],
34
+ "sideEffects": [
35
+ "**/*.css"
36
+ ],
37
+ "scripts": {
38
+ "dev": "vite",
39
+ "build": "tsc -p tsconfig.build.json && vite build",
40
+ "build:watch": "vite build --watch",
41
+ "preview": "vite preview",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest",
44
+ "test:coverage": "vitest run --coverage",
45
+ "lint": "eslint src --ext .ts,.tsx",
46
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
47
+ "format": "prettier --write \"src/**/*.{ts,tsx,css,md}\"",
48
+ "typecheck": "tsc --noEmit",
49
+ "storybook": "storybook dev -p 6006",
50
+ "build-storybook": "storybook build",
51
+ "prepublishOnly": "npm run lint && npm run test && npm run build",
52
+ "release": "npm run prepublishOnly && npm publish --access public"
53
+ },
54
+ "peerDependencies": {
55
+ "react": ">=17.0.0",
56
+ "react-dom": ">=17.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@storybook/addon-docs": "^8.0.0",
60
+ "@storybook/addon-essentials": "^8.0.0",
61
+ "@storybook/addon-interactions": "^8.0.0",
62
+ "@storybook/addon-links": "^8.0.0",
63
+ "@storybook/react": "^8.0.0",
64
+ "@storybook/react-vite": "^8.0.0",
65
+ "@storybook/testing-library": "^0.2.2",
66
+ "@testing-library/jest-dom": "^6.4.0",
67
+ "@testing-library/react": "^15.0.0",
68
+ "@testing-library/user-event": "^14.5.2",
69
+ "@types/node": "^25.3.2",
70
+ "@types/react": "^18.2.79",
71
+ "@types/react-dom": "^18.2.25",
72
+ "@typescript-eslint/eslint-plugin": "^7.6.0",
73
+ "@typescript-eslint/parser": "^7.6.0",
74
+ "@vitejs/plugin-react": "^4.2.1",
75
+ "@vitest/coverage-v8": "^0.34.6",
76
+ "eslint": "^8.57.0",
77
+ "eslint-config-prettier": "^9.1.0",
78
+ "eslint-plugin-react": "^7.34.1",
79
+ "eslint-plugin-react-hooks": "^4.6.0",
80
+ "eslint-plugin-storybook": "^0.8.0",
81
+ "jsdom": "^24.0.0",
82
+ "prettier": "^3.2.5",
83
+ "react": "^18.2.0",
84
+ "react-dom": "^18.2.0",
85
+ "storybook": "^8.0.0",
86
+ "typescript": "^5.4.5",
87
+ "vite": "^5.2.8",
88
+ "vite-plugin-dts": "^3.9.1",
89
+ "vitest": "^0.34.6"
90
+ }
91
+ }