chore: auto merge branches (#53249)

chore: sync master into feature
This commit is contained in:
github-actions[bot] 2025-03-22 06:34:57 +00:00 committed by GitHub
commit 103f82b6db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 841 additions and 296 deletions

80
.cursor/rules/demo.mdc Normal file
View File

@ -0,0 +1,80 @@
---
description:
globs: components/*/demo/**
alwaysApply: false
---
# Demo 规范
- demo 代码尽可能简洁
- 避免冗余代码,方便用户复制到项目直接使用
- 每个 demo 聚焦展示一个功能点
- 提供中英文两个版本的说明
- demo 文件扩展名:
- 基础 demo.tsx
- markdown 说明:.md
- 遵循展示优先原则,确保视觉效果良好
- 展示组件的主要使用场景
- 按照由简到繁的顺序排列 demo
- 确保 demo 在各种尺寸下都能正常展示
- 对于复杂交互提供必要的操作说明
## 文件组织
- 每个组件演示包含 `.md`(说明文档)和 `.tsx`(实际代码)两个文件
- 位置:组件目录下的 `demo` 子目录,如 `components/button/demo/`
- 命名:短横线连接的小写英文单词,如 `basic.tsx`、`custom-filter.tsx`
- 文件名应简洁地描述示例内容
## MD 文档规范
- 必须包含 `## zh-CN` 和 `## en-US` 两种语言说明
- 内容简洁明了,突出组件特性和用法
- 避免冗长段落,必要时使用列表或粗体
- 标注注意事项和实验性功能
## TSX 代码规范
- 导入顺序React → 依赖库 → 组件库 → 自定义组件 → 类型 → 样式
- 类型:为复杂数据定义清晰接口,避免 `any`
- 结构:
- 使用函数式组件和 Hooks
- 复杂逻辑提取为独立函数
- 状态管理用 `useState`/`useEffect`
- 风格:
- 2空格缩进箭头函数驼峰命名
- 常量大写+下划线布尔props用`is`/`has`前缀
## 示例类型
1. **基础用法**:展示最简用法,置于首位,代码精简
2. **变体展示**:不同大小、类型、状态,合理分组
3. **交互演示**:展示状态变化和用户交互
4. **数据交互**:展示加载、过滤、排序等
5. **边界情况**:空数据、错误状态处理
## 代码质量
- 实用且专注于单一功能
- 关键处添加简洁注释
- 使用有意义的数据和变量
- 优先使用 antd 内置组件,减少外部依赖
- 性能优化:适当使用 `useMemo`/`useCallback`,清理副作用
## 命名规则
- 基础文件:`basic.tsx`、`controlled.tsx` 等
- 调试类:使用 `debug-` 前缀
- 实验性:使用 `experimental-` 前缀
## 特殊示例
- **调试示例**:用于开发测试,通常不在文档显示
- **无障碍**:展示标签关联和键盘导航
- **响应式**:展示不同视口下的行为
- **多语言**:展示国际化配置方法
## 质量要求
- 确保代码运行正常,无控制台错误
- 适配常见浏览器
- 避免过时 API及时更新到新推荐用法

60
.cursor/rules/docs.mdc Normal file
View File

@ -0,0 +1,60 @@
---
description: 规范项目文档和 Changelog
globs: ["**/CHANGELOG*.md", "components/**/index.*.md"]
alwaysApply: false
---
# 文档和 Changelog 规范
## 基本要求
- 提供中英文两个版本
- 新的属性需要声明可用的版本号
- 属性命名符合 antd 的 API 命名规则https://github.com/ant-design/ant-design/wiki/API-Naming-rules
## Changelog 规范
- 在 CHANGELOG.en-US.md 和 CHANGELOG.zh-CN.md 书写每个版本的变更
- 对用户使用上无感知的改动建议(文档修补、微小的样式优化、代码风格重构等等)不要提及,保持 CHANGELOG 的内容有效性
- 非 antd 组件的改动(如工程化、构建工具、开发流程等)在 changelog 区块中写 "-"
- 用面向开发者的角度和叙述方式撰写 CHANGELOG不描述修复细节描述问题和对开发者的影响描述用户的原始问题而非你的解决方式
### 示例
- 不好的例子:修复组件 Typography 的 dom 结构问题
- 好的例子:修复了 List.Item 中内容空格丢失的问题
### 其他要求
- 新增属性时,建议用易于理解的语言补充描述用户可以感知的变化
- 尽量给出原始的 PR 链接,社区提交的 PR 改动加上提交者的链接
- 底层模块升级中间版本要去 rc-component 里找到改动,给出变动说明
- 建议参考之前版本的日志写法
- 将同组件的改动放在一起,内容子级缩进
### Changelog Emoji 规范
- 🐞 Bug 修复
- 💄 样式更新或 token 更新
- 🆕 新增特性,新增属性
- 🔥 极其值得关注的新增特性
- 🇺🇸🇨🇳🇬🇧 国际化改动
- 📖 📝 文档或网站改进
- ✅ 新增或更新测试用例
- 🛎 更新警告/提示信息
- ⌨️ ♿ 可访问性增强
- 🗑 废弃或移除
- 🛠 重构或工具链优化
- ⚡️ 性能提升
# 文档规范
- 提供中英文两个版本
- 新属性需声明可用的版本号
- 属性命名符合 API 命名规则
- 组件文档包含使用场景、基础用法、API 说明
- 文档示例应简洁明了
- 属性的描述应清晰易懂
- 对复杂功能提供详细说明
- 加入 TypeScript 定义
- 提供常见问题解答
- 更新文档时同步更新中英文版本

128
.cursor/rules/git.mdc Normal file
View File

@ -0,0 +1,128 @@
---
description:
globs:
alwaysApply: true
---
# Git 规范
## 分支管理
- 禁止直接提交到以下保护分支:
- `master`:主分支,用于发布
- `feature`:特性分支,用于开发新版本
- `next`:下一个版本分支
## 开发流程
1. 从保护分支(通常是 `master`)创建新的功能分支
2. 在新分支上进行开发
3. 提交 Pull Request 到目标分支
4. 等待 Code Review 和 CI 通过
5. 合并到目标分支
## 分支命名规范
- 功能开发:`feat/description-of-feature`
- 例如:`feat/add-dark-mode`
- 例如:`feat/improve-table-performance`
- 问题修复:`fix/issue-number-or-description`
- 例如:`fix/button-style-issue`
- 例如:`fix/issue-1234`
- 文档更新:`docs/what-is-changed`
- 例如:`docs/update-api-reference`
- 例如:`docs/fix-typos`
- 代码重构:`refactor/what-is-changed`
- 例如:`refactor/button-component`
- 例如:`refactor/remove-deprecated-api`
- 样式修改:`style/what-is-changed`
- 例如:`style/update-button-tokens`
- 例如:`style/improve-mobile-layout`
- 测试相关:`test/what-is-changed`
- 例如:`test/add-button-test`
- 例如:`test/improve-coverage`
- 构建相关:`build/what-is-changed`
- 例如:`build/upgrade-webpack`
- 例如:`build/fix-ts-config`
- 持续集成:`ci/what-is-changed`
- 例如:`ci/add-e2e-test`
- 例如:`ci/fix-deploy-script`
- 性能优化:`perf/what-is-changed`
- 例如:`perf/optimize-render`
- 例如:`perf/reduce-bundle-size`
- 依赖升级:`deps/package-name-version`
- 例如:`deps/upgrade-react-18`
- 例如:`deps/update-dependencies`
## 分支命名注意事项
1. 使用小写字母
2. 使用连字符(-)分隔单词
3. 简短但具有描述性
4. 避免使用下划线或其他特殊字符
5. 如果与 Issue 关联,可以包含 Issue 编号
## Pull Request 规范
### PR 标题
- PR 标题始终使用英文
- 遵循格式:`类型: 简短描述`
- 例如:`fix: fix button style issues in Safari browser`
- 例如:`feat: add dark mode support`
### PR 内容
- PR 内容默认使用英文
- 尽量简洁清晰地描述改动内容和目的
- 可以视需要在英文描述后附上中文说明
### PR 模板
提交 PR 时请使用项目中提供的模板:
- 英文模板(推荐):[PULL_REQUEST_TEMPLATE.md](mdc:.github/PULL_REQUEST_TEMPLATE.md)
- 中文模板:[PULL_REQUEST_TEMPLATE_CN.md](mdc:.github/PULL_REQUEST_TEMPLATE_CN.md)
### PR 提交注意事项
1. **合并策略**
- 新特性请提交至 `feature` 分支
- 其余可提交至 `master` 分支
2. **审核流程**
- PR 需要由至少一名维护者审核通过后才能合并
- 确保所有 CI 检查都通过
- 解决所有 Code Review 中提出的问题
3. **PR 质量要求**
- 确保代码符合项目代码风格
- 添加必要的测试用例
- 更新相关文档
- 大型改动需要更详细的说明和更多的审核者参与
4. **工具标注**
- 如果是用 Cursor 提交的代码,请在 PR body 末尾进行标注:`> Submitted by Cursor`
## 新增内容
- Pull Request 标题格式:[组件名]: 描述
- 从 master 分支创建新分支
- 分支命名规范:
- feature/xxx新特性
- fix/xxxBug 修复
- docs/xxx文档更新
- PR 说明中选择改动类型:
- 🆕 新特性提交
- 🐞 Bug 修复
- 📝 文档改进
- 📽️ 演示代码改进
- 💄 样式/交互改进
- 🤖 TypeScript 更新
- 📦 包体积优化
- ⚡️ 性能优化
- 🌐 国际化改进
- 提供改动背景和解决方案
- 更新日志同时提供英文和中文版本

108
.cursor/rules/naming.mdc Normal file
View File

@ -0,0 +1,108 @@
---
description:
globs:
alwaysApply: true
---
Basically, antd naming requires **FULL NAME** instead of Abbreviation.
## Props
* Initialize prop: `default` + `PropName`
* Force render: `forceRender`
* Force render sub component: `force` + `Sub Component Name` + `Render`
* Sub Component Render: `Sub Component Name` + `Render`. e.g.
> panelRender(originNode, info: { SubComponent1, SubComponent2, [somePassedProps]: someValue })
* Sub Item Render: `Sub Item Name` + `Render`. e.g.
> cellRender(date, info: { [somePassedProps]: someValue })
* Data Source: `dataSource`
* Panel open: popup & dropdown `open`, additional popup `popupName` + `Open` like `tooltipOpen`
* Do not use `visible` to make sure all the visible api align
* `children`:
* Mainly display content. To avoid additional prop name.
* Option list like `Select.Option` or `Tree.TreeNode`.
* Customize wrapped component can consider use `component` prop if `children` may have other usage in future.
* Display related naming: `show` + `PropName`
* Functional: `PropName` + `able`
* Disable: `disabled`
* sub component: `disabled` + `Sub Component Name`
* Extra: `extra`
* sub component: `Sub Component Name` + `extra`. e.g. `titleExtra`
* mainly icon: `icon`
* Merge with function first: `functionName: { icon }`. e.g. `expandable: { icon: <Smile /> }`
* Multiple icons: `FunctionName` + `Icon`
* Trigger: `trigger`
* Sub function trigger: `Sub Function` + `Trigger`
* Trigger on the time point: `xxx` + `On` + `EventName` (e.g. `destroyOnClose`)
* Component use other component config. Naming as component.(e.g. `<Table pagination={{...}} />`)
* ClassName: `className`
* Additional classes should be merged into `classes` (e.g. `<Button classes={{ inner: 'custom-inner' }} />`)
* Format
* Not modify value when blur: `preserveInvalidOnBlur`
## Event
* Trigger event: `on` + `EventName`
* Trigger sub component event: `on` + `SubComponentName` + `EventName` (e.g.`onSearchChange`)
* Trigger prop event: `on` + `PropName` + `EventName` (e.g.`onDragStart`)
* Before trigger event: `before` + `EventName`
* After trigger event: `after` + `EventName`
* After continuous action, such as drag Slider: `on` + `EventName` + `Complete`
## Reference
Component should have `ref` prop. Which should provide the structure:
```tsx
ComponentRef {
nativeElement: HTMLElement;
// Other function
focus: VoidFunction;
}
```
## Component Token
`variant (optional)` + `semantic part` + `semantic part variant (optional)` + `css property` + `size/disabled (optional)`
All component tokens should follow the structure above, and should not conflict with Global Token.
* `variant` means this token only works in certain variant, like `horizontal` `borderless`.
* `semantic part` means the typical element of component, like `item` `header`. If there's.
* `semantic part status` means the variant of the semantic part before it, like `hover` `selected`.
* `css property` means the exact property where the token is used, like `fontSize` `width`.
For example:
| v4 | v5 | Note |
| --- | --- | --- |
| `@menu-item-color` | `itemColor` | Remove the component prefix |
| `@select-item-selected-bg` | `itemSelectedBg` | `selected` is variant of item |
| `@select-single-item-height-lg` | `itemHeightLG` | `single` is variant of Select (by default), `LG` is size of Select |
> Note: If there's no semantic part for one component token, for example, the root borderRadius of Button, it is not suitable to add it. Because we could modify it easily with `className` and `style`.
## Current listing api & Chinese version
ref: [#16048](mdc:https:/github.com/ant-design/ant-design/issues/16048)
## API standard in the document
### Examples
| Property | Description | Type | Default |
| --------- | ---------------------- | ------------------------------ | ------ |
| htmlType | xxx | string | `button ` |
| type | xxx | `horizontal ` \| `vertical ` | `horizontal` |
| disabled | xxx | boolean | false |
| minLength | xxx | number | 0 |
| style | xxx | CSSProperties | - |
| character | xxx | (props) => ReactNode | - |
| offset | xxx| \[number, number] | \[0, 0] |
| value | xxx | string \| number | `small` |
### Promise
- When string type, the **Default** use ` `` `.
- Can also list string optional values in **Type**.
- When boolean type, the **Default** value is true or false.
- When number type, the **Default** value use numbers directly.
- When function type, use an arrow function expression in **Type**.
- No default value use - .
- Capitalize the first letter in **Description** apart from `someProp`.
- No period at the end of the **Description**.
- API order is arranged in alphabetical order, and can be put together under special circumstances (such as: xs sm md).
ref: [#25066](mdc:https:/github.com/ant-design/ant-design/issues/25066)

27
.cursor/rules/project.mdc Normal file
View File

@ -0,0 +1,27 @@
---
description:
globs:
alwaysApply: true
---
# 项目背景
这是 ant-design/ant-designantd的源代码仓库是一个 React 组件库,发布为 npm 包 antd。
- 使用 TypeScript 和 React 开发
- 兼容 React 16 ~ 19 版本
- 组件库设计精美,功能完善,广泛应用于企业级中后台产品
- 遵循 Ant Design 设计规范
- 支持国际化
# 编码规范
- 使用 TypeScript 和 React 书写
- 使用函数式组件和 hooks避免类组件
- 使用提前返回early returns提高代码可读性
- 避免引入新依赖,严控打包体积
- 兼容 Chrome 80+ 浏览器
- 支持服务端渲染
- 保持向下兼容,避免 breaking change
- 组件名使用大驼峰PascalCase
- 属性名使用小驼峰camelCase
- 合理使用 React.memo、useMemo 和 useCallback 优化性能

79
.cursor/rules/styling.mdc Normal file
View File

@ -0,0 +1,79 @@
---
description:
globs: components/*/style/**
alwaysApply: false
---
# 样式规范
## 样式方案
- 使用 `@ant-design/cssinjs` 作为样式解决方案
- 每个组件的样式应该放在 `style/` 目录下
- 样式文件应该与组件结构保持一致
- 使用 CSS-in-JS 时应当注意性能影响,避免不必要的样式重计算
- 样式生成函数应遵循 `gen[ComponentName]Style` 的命名规范
- 样式覆盖应使用类选择器而非标签选择器,提高样式特异性
## Token 系统
- 使用 Ant Design 的设计 Token 系统
- 避免硬编码颜色、尺寸、间距等值
- 组件样式应基于全局 Token 和组件级 Token
- 自定义样式应尽可能使用现有的 Token保持一致性
- 组件级 Token 命名规范:`Component` + 属性名,如 `buttonPrimaryColor`
- 对 Token 的修改应当向下传递,确保设计系统的一致性
## 响应式设计
- 组件应支持在不同屏幕尺寸下良好展示
- 使用相对单位(如 em、rem而非固定像素值
- 关键断点应与设计系统保持一致
- 在小屏幕上提供良好的降级方案
- 使用 CSS Grid 和 Flexbox 布局实现响应式布局
- 考虑移动设备上的触摸交互体验
## 暗色模式
- 所有组件必须支持暗色模式
- 暗色模式应通过 Token 系统实现,不应硬编码
- 测试暗色模式下的颜色对比度,确保可访问性
- 在设计暗色模式时考虑降低亮度和饱和度
- 确保文本在暗色背景上有足够的对比度
- 图片和图标应提供适合暗色模式的版本
## RTL 支持
- 组件应支持从右到左RTL的阅读方向
- 使用 CSS 逻辑属性(如 margin-inline-start替代方向性属性如 margin-left
- 图标和方向性元素应随 RTL 模式翻转
- 测试组件在 RTL 模式下的布局和交互
- 确保文本对齐和方向符合 RTL 规范
- 处理好数字和日期等特殊内容在 RTL 模式下的显示
## 动画效果
- 使用 CSS 过渡实现简单动画
- 复杂动画使用 rc-motion 实现
- 尊重用户的减少动画设置prefers-reduced-motion
- 动画时长和缓动函数应保持一致性
- 动画不应干扰用户的操作和阅读体验
- 为关键操作提供合适的反馈动画
- 避免使用会导致性能问题的 CSS 属性(如 box-shadow进行动画
## 主题定制
- 支持通过 ConfigProvider 进行主题定制
- 提供完整的组件级 Token 配置
- 保持向后兼容性,不轻易改变 Token 含义
- 避免在组件内使用不可覆盖的样式
- 提供主题切换的平滑过渡效果
- 测试自定义主题在各种组件组合下的效果
## 可访问性样式
- 遵循 WCAG 2.1 AA 级别标准
- 确保焦点状态有明显的视觉提示
- 提供足够的色彩对比度
- 不依赖颜色来传达信息
- 支持用户放大页面至 200% 时的正常布局
- 避免使用会导致闪烁的动画

11
.cursor/rules/testing.mdc Normal file
View File

@ -0,0 +1,11 @@
---
description:
globs: **/__tests__/**,**/*.test.tsx,**/*.test.ts
alwaysApply: false
---
# 测试规范
- 使用 Jest 和 React Testing Library 编写单元测试
- 对 UI 组件使用快照测试 (Snapshot Testing)
- 测试覆盖率要求 100%
- 测试文件放在 __tests__ 目录命名格式为index.test.tsx 或 xxx.test.tsx

View File

@ -0,0 +1,81 @@
# TypeScript 规范
## 基本原则
- 所有组件和函数必须提供准确的类型定义
- 避免使用 `any` 类型,尽可能精确地定义类型
- 使用接口而非类型别名定义对象结构
- 导出所有公共接口类型,方便用户使用
- 严格遵循 TypeScript 类型设计原则,确保类型安全
- 确保编译无任何类型错误或警告
## 组件类型定义
- 组件 props 应使用 interface 定义,便于扩展
- 组件 props 接口命名应为 `ComponentNameProps`
- 为组件状态定义专门的接口,如 `ComponentNameState`
- 复杂的数据结构应拆分为多个接口定义
- 组件的 ref 类型应该明确定义,使用 `React.ForwardRefRenderFunction`
- 所有回调函数类型应明确定义参数和返回值
## 泛型使用
- 适当使用泛型增强类型灵活性
- 为泛型参数提供合理的默认类型和约束
- 避免过度使用泛型导致类型复杂化
- 在泛型参数上应用限制条件constraints确保类型安全
- 为复杂泛型提供类型别名以提高可读性
## 类型合并与扩展
- 使用交叉类型(&)合并多个类型
- 使用 Partial<T>、Pick<T, K>、Omit<T, K> 等工具类型修改现有类型
- 扩展原生 DOM 元素属性时,继承相应的内置类型
- 使用 type 定义联合类型和交叉类型
- 优先使用自带的工具类型,避免重复定义
## 枚举和常量
- 使用字面量联合类型定义有限的选项集合
- 为复杂的枚举值提供类型守卫函数
- 避免使用 `enum`,优先使用联合类型和 `as const`
- 对于关键常量,使用 `as const` 断言确保类型严格
- 为联合类型中的每个值提供适当的注释
## 类型推断与断言
- 尽可能依赖 TypeScript 的类型推断
- 只在必要时使用类型断言as
- 使用类型守卫函数进行运行时类型检查
- 避免使用非空断言操作符(!
- 使用 `instanceof` 和 `typeof` 进行类型守卫
- 为自定义类型创建类型谓词type predicates函数
## JSDoc 注释
- 为复杂的类型、函数、组件添加 JSDoc 注释
- 使用 `@deprecated` 标记已废弃的 API
- 在注释中提供使用示例
- 说明参数和返回值的含义与约束
- 在 interface 和重要类型定义上添加文档注释
- 使用 `@template` 标记泛型参数
## 类型兼容性
- 确保类型定义兼容不同版本的 React
- 避免使用实验性或不稳定的 TypeScript 特性
- 为第三方库未提供的类型编写声明文件
- 使用条件类型处理复杂的类型逻辑
- 验证类型在不同 TypeScript 版本下的兼容性
## 严格使用 TypeScript 类型
- 导出组件类型和接口
- 使用 React.FC<Props> 或明确的返回类型
- 避免使用 any优先使用 unknown
- 组件 Props 使用 interface 定义
- 工具类型使用 type 定义
- 使用明确的命名约定
- 合理使用泛型提高复用性
- 导出类型时使用 export type
- 组件属性使用 JSDoc 注释说明用途

View File

@ -1,76 +0,0 @@
## 项目背景
这是 ant-design/ant-designantd的源代码仓库是一个 React 组件库,发布为 npm 包 antd 。
## 编码规范
- 使用 TypeScript 和 React 书写
- 兼容 React 16 ~ 19 版本
- 使用 @ant-design/cssinjs 书写 css
- 使用 @ant-design/icons 图标库
- 使用函数式组件和 hooks避免类组件
- 尽可能使用提前返回early returns以提高代码的可读性
- 避免引入新的依赖,严控打包体积
- demo 代码尽可能简洁,避免冗余代码,方便用户复制到应用项目里直接可用
- 兼容 chrome 80+ 浏览器
- 修改时请保持向下兼容,避免制造 break change
## 测试要求
- 使用 Jest 和 React Testing Library 编写单元测试
- 对 UI 组件使用快照测试 (Snapshot Testing)
- 测试覆盖率要求 100%
# 文档规范
- 提供中英文两个版本
- 新的属性需要声明可用的版本号
- 属性命名符合 antd 的 API 命名规则https://github.com/ant-design/ant-design/wiki/API-Naming-rules
## Changelog 规范
- 在 CHANGELOG.en-US.md 和 CHANGELOG.zh-CN.md 书写每个版本的变更
- 对用户使用上无感知的改动建议(文档修补、微小的样式优化、代码风格重构等等)不要提及,保持 CHANGELOG 的内容有效性。
- 用面向开发者的角度和叙述方式撰写 CHANGELOG不描述修复细节描述问题和对开发者的影响描述用户的原始问题而非你的解决方式。
* 例子一
bad: 修复组件 Typography 的 dom 结构问题。
good: 重构并简化了 List Item 的 dom 结构,并且修复了 Item 中内容空格丢失的问题。
* 例子二
bad: 修复 lib 下样式文件路径问题。
good: 修复部分组件样式丢失的问题。
- 新增属性时,建议用易于理解的语言补充描述用户可以感知的变化。(例如,新增 onCellClick 属性,可以定义单元格点击事件)
- 尽量给出原始的 PR 链接,社区提交的 PR 改动加上提交者的链接。
- 底层模块升级中间版本要去 rc-component 里找到改动,给出变动说明。
- 建议参考之前版本的日志写法
- 将同组件的改动放在一起,内容子级缩进。
- 每一个改动前加 emoji 增加更新日志的可读性和生动性,可选 emoji 参考:
- 🐞 Bug 修复
- 💄 样式更新或 token 更新
- 🆕 新增特性,新增属性
- 🔥 极其值得关注的新增特性
- 🇺🇸🇨🇳🇬🇧 国际化改动,注意这里直接用对应国家/地区的旗帜。
- 📖 📝 文档或网站改进
- ✅ 新增或更新测试用例
- 🛎 更新警告/提示信息
- ⌨️ ♿ 可访问性增强
- 🗑 废弃或移除
- 🛠 重构或工具链优化
- ⚡️ 性能提升
## 实现要求
你是一名资深的前端开发工程师,同时也是 React、Ant Design、TypeScript、HTML、CSS 的专家。你思维缜密,能够给出细致入微的答案,并且在逻辑推理方面表现出色。你会谨慎地提供准确、真实、深思熟虑的答案,并且擅长逻辑推理。
- 严格遵循用户的需求,逐字逐句完成。
- 首先逐步思考——用伪代码详细描述你要构建的内容的计划。
- 确认后,再编写代码!
- 始终编写正确、符合最佳实践、遵循 DRY 原则(不要重复自己)、无错误、功能齐全且可运行的代码。
- 注重代码的易读性和可维护性。
- 非常关注代码压缩后的体积。
- 全面实现所有请求的功能。
- 不留任何待办事项、占位符或缺失的部分。
- 确保代码完整!彻底验证最终结果。
- 包含所有必要的导入,并确保关键组件的命名得当。
- 简洁明了,尽量减少其他冗长的说明。
- 如果你认为可能没有正确的答案,请明确说明。
- 如果你不知道答案,请坦诚说明,而不是猜测。

View File

@ -1,6 +1,6 @@
import * as React from 'react';
// @ts-ignore
import { TinyColor } from 'dumi-plugin-color-chunk/component';
import { FastColor } from '@ant-design/fast-color';
import type { ColorInput } from '@ant-design/fast-color';
import { createStyles } from 'antd-style';
const useStyle = createStyles(({ token, css }) => ({
@ -22,17 +22,14 @@ const useStyle = createStyles(({ token, css }) => ({
}));
interface ColorChunkProps {
value: any;
value: ColorInput;
}
const ColorChunk: React.FC<React.PropsWithChildren<ColorChunkProps>> = (props) => {
const { styles } = useStyle();
const { value, children } = props;
const dotColor = React.useMemo(() => {
const _color = new TinyColor(value).toHex8String();
return _color.endsWith('ff') ? _color.slice(0, -2) : _color;
}, [value]);
const dotColor = React.useMemo(() => new FastColor(value).toHexString(), [value]);
return (
<span className={styles.codeSpan}>

View File

@ -72,6 +72,7 @@ export default function useResponsiveObserver() {
let screens: Partial<Record<Breakpoint, boolean>> = {};
return {
responsiveMap,
matchHandlers: {} as {
[prop: string]: {
mql: MediaQueryList;
@ -98,6 +99,18 @@ export default function useResponsiveObserver() {
this.unregister();
}
},
register() {
Object.keys(responsiveMap).forEach((screen) => {
const matchMediaQuery = responsiveMap[screen as Breakpoint];
const listener = ({ matches }: { matches: boolean }) => {
this.dispatch({ ...screens, [screen]: matches });
};
const mql = window.matchMedia(matchMediaQuery);
mql.addListener(listener);
this.matchHandlers[matchMediaQuery] = { mql, listener };
listener(mql);
});
},
unregister() {
Object.keys(responsiveMap).forEach((screen) => {
const matchMediaQuery = responsiveMap[screen as Breakpoint];
@ -106,25 +119,6 @@ export default function useResponsiveObserver() {
});
subscribers.clear();
},
register() {
Object.keys(responsiveMap).forEach((screen) => {
const matchMediaQuery = responsiveMap[screen as Breakpoint];
const listener = ({ matches }: { matches: boolean }) => {
this.dispatch({
...screens,
[screen]: matches,
});
};
const mql = window.matchMedia(matchMediaQuery);
mql.addListener(listener);
this.matchHandlers[matchMediaQuery] = {
mql,
listener,
};
listener(mql);
});
},
responsiveMap,
};
}, [token]);
}

View File

@ -194,8 +194,8 @@ const genArrowsStyle: GenerateStyle<CarouselToken> = (token) => {
width: arrowLength,
height: arrowLength,
border: `0 solid currentcolor`,
borderInlineWidth: '2px 0',
borderBlockWidth: '2px 0',
borderInlineStartWidth: 2,
borderBlockStartWidth: 2,
borderRadius: 1,
content: '""',
},

View File

@ -5,9 +5,10 @@ import type { ColorPickerToken } from './index';
/**
* @private Internal usage only
* see: https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/conic-gradient#checkerboard
*/
export const getTransBg = (size: string, colorFill: string): CSSObject => ({
backgroundImage: `conic-gradient(${colorFill} 0 25%, transparent 0 50%, ${colorFill} 0 75%, transparent 0)`,
backgroundImage: `conic-gradient(${colorFill} 25%, transparent 25% 50%, ${colorFill} 50% 75%, transparent 75% 100%)`,
backgroundSize: `${size} ${size}`,
});

View File

@ -321,8 +321,8 @@ export const genPanelStyle = (token: SharedPickerToken): CSSObject => {
width: pickerControlIconSize,
height: pickerControlIconSize,
border: `0 solid currentcolor`,
borderBlockWidth: `${unit(pickerControlIconBorderWidth)} 0`,
borderInlineWidth: `${unit(pickerControlIconBorderWidth)} 0`,
borderBlockStartWidth: pickerControlIconBorderWidth,
borderInlineStartWidth: pickerControlIconBorderWidth,
content: '""',
},
},
@ -337,8 +337,8 @@ export const genPanelStyle = (token: SharedPickerToken): CSSObject => {
width: pickerControlIconSize,
height: pickerControlIconSize,
border: '0 solid currentcolor',
borderBlockWidth: `${unit(pickerControlIconBorderWidth)} 0`,
borderInlineWidth: `${unit(pickerControlIconBorderWidth)} 0`,
borderBlockStartWidth: pickerControlIconBorderWidth,
borderInlineStartWidth: pickerControlIconBorderWidth,
content: '""',
},
},

View File

@ -31,7 +31,7 @@ const floatButtonGroupMotion = (token: FloatButtonToken) => {
});
const moveRightIn = new Keyframes('antFloatButtonMoveRightIn', {
'0%': {
transform: `translate3d(${calc(floatButtonSize).mul(-1).equal()}, 0, 0)`,
transform: `translate3d(${unit(calc(floatButtonSize).mul(-1).equal())}, 0, 0)`,
transformOrigin: '0 0',
opacity: 0,
},
@ -48,14 +48,14 @@ const floatButtonGroupMotion = (token: FloatButtonToken) => {
opacity: 1,
},
'100%': {
transform: `translate3d(${calc(floatButtonSize).mul(-1).equal()}, 0, 0)`,
transform: `translate3d(${unit(calc(floatButtonSize).mul(-1).equal())}, 0, 0)`,
transformOrigin: '0 0',
opacity: 0,
},
});
const moveBottomIn = new Keyframes('antFloatButtonMoveBottomIn', {
'0%': {
transform: `translate3d(0, ${calc(floatButtonSize).mul(-1).equal()}, 0)`,
transform: `translate3d(0, ${unit(calc(floatButtonSize).mul(-1).equal())}, 0)`,
transformOrigin: '0 0',
opacity: 0,
},
@ -72,7 +72,7 @@ const floatButtonGroupMotion = (token: FloatButtonToken) => {
opacity: 1,
},
'100%': {
transform: `translate3d(0, ${calc(floatButtonSize).mul(-1).equal()}, 0)`,
transform: `translate3d(0, ${unit(calc(floatButtonSize).mul(-1).equal())}, 0)`,
transformOrigin: '0 0',
opacity: 0,
},

View File

@ -283,7 +283,7 @@ const genFormItemStyle: GenerateStyle<FormToken> = (token) => {
marginInlineStart: token.marginXXS,
color: token.colorTextDescription,
[`&.${formItemCls}-required-mark-hidden`]: {
[`&${formItemCls}-required-mark-hidden`]: {
display: 'none',
},
},

View File

@ -1,6 +1,11 @@
import { unit } from '@ant-design/cssinjs';
import { genBasicInputStyle, genInputGroupStyle, genPlaceholderStyle, initInputToken } from '../../input/style';
import {
genBasicInputStyle,
genInputGroupStyle,
genPlaceholderStyle,
initInputToken,
} from '../../input/style';
import {
genBorderlessStyle,
genFilledGroupStyle,
@ -235,7 +240,6 @@ const genInputNumberStyles: GenerateStyle<InputNumberToken> = (token: InputNumbe
'&[type="number"]::-webkit-inner-spin-button, &[type="number"]::-webkit-outer-spin-button':
{
margin: 0,
webkitAppearance: 'none',
appearance: 'none',
},
},

View File

@ -17,7 +17,6 @@ import type { SizeType } from '../config-provider/SizeContext';
import { FormItemInputContext } from '../form/context';
import useVariant from '../form/hooks/useVariants';
import { useCompactItemContext } from '../space/Compact';
import useHandleResizeWrapper from './hooks/useHandleResizeWrapper';
import type { InputFocusOptions } from './Input';
import { triggerFocus } from './Input';
import { useSharedStyle } from './style';
@ -57,6 +56,8 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
styles,
variant: customVariant,
showCount,
onMouseDown,
onResize,
...rest
} = props;
@ -76,11 +77,11 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
styles: contextStyles,
} = useComponentConfig('textArea');
// ===================== Disabled =====================
// =================== Disabled ===================
const disabled = React.useContext(DisabledContext);
const mergedDisabled = customDisabled ?? disabled;
// ===================== Status =====================
// ==================== Status ====================
const {
status: contextStatus,
hasFeedback,
@ -88,7 +89,7 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
} = React.useContext(FormItemInputContext);
const mergedStatus = getMergedStatus(contextStatus, customStatus);
// ===================== Ref =====================
// ===================== Ref ======================
const innerRef = React.useRef<RcTextAreaRef>(null);
React.useImperativeHandle(ref, () => ({
@ -101,12 +102,12 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
const prefixCls = getPrefixCls('input', customizePrefixCls);
// ===================== Style =====================
// ==================== Style =====================
const rootCls = useCSSVarCls(prefixCls);
const [wrapSharedCSSVar, hashId, cssVarCls] = useSharedStyle(prefixCls, rootClassName);
const [wrapCSSVar] = useStyle(prefixCls, rootCls);
// ===================== Compact Item =====================
// ================= Compact Item =================
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
// ===================== Size =====================
@ -116,8 +117,39 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
const mergedAllowClear = getAllowClear(allowClear ?? contextAllowClear);
const handleResizeWrapper = useHandleResizeWrapper();
// ==================== Resize ====================
// https://github.com/ant-design/ant-design/issues/51594
const [isMouseDown, setIsMouseDown] = React.useState(false);
// When has wrapper, resize will make as dirty for `resize: both` style
const [resizeDirty, setResizeDirty] = React.useState(false);
const onInternalMouseDown: typeof onMouseDown = (e) => {
setIsMouseDown(true);
onMouseDown?.(e);
const onMouseUp = () => {
setIsMouseDown(false);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mouseup', onMouseUp);
};
const onInternalResize: RcTextAreaProps['onResize'] = (size) => {
onResize?.(size);
// Change to dirty since this maybe from the `resize: both` style
if (isMouseDown && typeof getComputedStyle === 'function') {
const ele = innerRef.current?.nativeElement?.querySelector('textarea');
if (ele && getComputedStyle(ele).resize === 'both') {
setResizeDirty(true);
}
}
};
// ==================== Render ====================
return wrapSharedCSSVar(
wrapCSSVar(
<RcTextArea
@ -134,6 +166,8 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
rootClassName,
compactItemClassnames,
contextClassName,
// Only for wrapper
resizeDirty && `${prefixCls}-textarea-affix-wrapper-resize-dirty`,
)}
classNames={{
...classes,
@ -146,6 +180,7 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
hashId,
classes?.textarea,
contextClassNames.textarea,
isMouseDown && `${prefixCls}-mouse-active`,
),
variant: classNames(
{
@ -159,7 +194,7 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-sm`]: mergedSize === 'small',
[`${prefixCls}-affix-wrapper-lg`]: mergedSize === 'large',
[`${prefixCls}-textarea-show-count`]: props.showCount || props.count?.show,
[`${prefixCls}-textarea-show-count`]: showCount || props.count?.show,
},
hashId,
),
@ -170,10 +205,8 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
}
showCount={showCount}
ref={innerRef}
onResize={(size) => {
rest.onResize?.(size);
showCount && handleResizeWrapper(innerRef.current);
}}
onResize={onInternalResize}
onMouseDown={onInternalMouseDown}
/>,
),
);

View File

@ -11956,6 +11956,26 @@ Array [
</button>
</span>
</span>,
<br />,
<span
class="ant-input-affix-wrapper ant-input-textarea-affix-wrapper ant-input-textarea-show-count ant-input-show-count ant-input-outlined"
data-count="0"
style="resize: both;"
>
<textarea
class="ant-input"
style="resize: both;"
/>
<span
class="ant-input-suffix"
>
<span
class="ant-input-data-count"
>
0
</span>
</span>
</span>,
]
`;

View File

@ -5264,6 +5264,26 @@ Array [
</button>
</span>
</span>,
<br />,
<span
class="ant-input-affix-wrapper ant-input-textarea-affix-wrapper ant-input-textarea-show-count ant-input-show-count ant-input-outlined"
data-count="0"
style="resize:both"
>
<textarea
class="ant-input"
style="resize:both"
/>
<span
class="ant-input-suffix"
>
<span
class="ant-input-data-count"
>
0
</span>
</span>
</span>,
]
`;

View File

@ -1,6 +1,5 @@
import type { ChangeEventHandler, TextareaHTMLAttributes } from 'react';
import React, { useState } from 'react';
import type { TextAreaRef as RcTextAreaRef } from 'rc-textarea';
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import Input from '..';
@ -10,12 +9,10 @@ import {
fireEvent,
pureRender,
render,
renderHook,
triggerResize,
waitFakeTimer,
waitFakeTimer19,
} from '../../../tests/utils';
import useHandleResizeWrapper from '../hooks/useHandleResizeWrapper';
import type { TextAreaRef } from '../TextArea';
const { TextArea } = Input;
@ -533,113 +530,21 @@ describe('TextArea allowClear', () => {
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('`bordered` is deprecated'));
errSpy.mockRestore();
});
});
describe('TextArea useHandleResizeWrapper', () => {
let requestAnimationFrameSpy: jest.SpyInstance;
it('resize: both', async () => {
const { container } = render(<TextArea showCount style={{ resize: 'both' }} />);
beforeAll(() => {
// Use fake timers to control requestAnimationFrame.
jest.useFakeTimers();
// Override requestAnimationFrame to simulate a 16ms delay.
requestAnimationFrameSpy = jest
.spyOn(window, 'requestAnimationFrame')
.mockImplementation((cb: FrameRequestCallback) => {
return window.setTimeout(() => cb(performance.now()), 16);
});
});
fireEvent.mouseDown(container.querySelector('textarea')!);
afterAll(() => {
jest.useRealTimers();
requestAnimationFrameSpy.mockRestore();
});
triggerResize(container.querySelector('textarea')!);
await waitFakeTimer();
it('does nothing when rcTextArea is null', () => {
const { result } = renderHook(useHandleResizeWrapper);
// Calling with null should not throw or change anything.
expect(() => result.current?.(null)).not.toThrow();
});
expect(container.querySelector('.ant-input-textarea-affix-wrapper')).toHaveClass(
'ant-input-textarea-affix-wrapper-resize-dirty',
);
expect(container.querySelector('.ant-input-mouse-active')).toBeTruthy();
it('does nothing when style width does not include "px"', () => {
const { result } = renderHook(useHandleResizeWrapper);
const fakeRcTextArea = {
resizableTextArea: {
textArea: {
style: {
width: '100', // missing 'px'
},
},
},
nativeElement: {
offsetWidth: 110,
style: {},
},
} as unknown as RcTextAreaRef;
result.current?.(fakeRcTextArea);
// Fast-forward time to see if any scheduled callback would execute.
jest.advanceTimersByTime(16);
// nativeElement.style.width remains unchanged.
expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined();
});
it('adjusts width correctly when offsetWidth is slightly greater than the textArea width (increased scenario)', () => {
const { result } = renderHook(useHandleResizeWrapper);
const fakeRcTextArea = {
resizableTextArea: {
textArea: {
style: {
width: '100px', // valid width with px
},
},
},
// offsetWidth is 101 so the difference is 1 (< ELEMENT_GAP of 2)
nativeElement: {
offsetWidth: 101,
style: {},
},
} as unknown as RcTextAreaRef;
// Immediately after calling handleResizeWrapper, the update is scheduled.
expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined();
result.current?.(fakeRcTextArea);
// Fast-forward time to trigger the requestAnimationFrame callback.
jest.advanceTimersByTime(16);
// Expected new width: 100 + 2 = 102px.
expect(fakeRcTextArea.nativeElement.style.width).toBe('102px');
});
it('adjusts width correctly when offsetWidth is significantly greater than the textArea width (decreased scenario)', () => {
const { result } = renderHook(useHandleResizeWrapper);
const fakeRcTextArea = {
resizableTextArea: {
textArea: {
style: {
width: '100px',
},
},
},
// offsetWidth is 105 so the difference is 5 (> ELEMENT_GAP of 2)
nativeElement: {
offsetWidth: 105,
style: {},
},
} as unknown as RcTextAreaRef;
// Immediately after calling handleResizeWrapper, the update is scheduled.
expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined();
result.current?.(fakeRcTextArea);
// Fast-forward time to trigger the requestAnimationFrame callback.
jest.advanceTimersByTime(16);
// Expected new width remains: 100 + 2 = 102px.
expect(fakeRcTextArea.nativeElement.style.width).toBe('102px');
fireEvent.mouseUp(container.querySelector('textarea')!);
expect(container.querySelector('.ant-input-mouse-active')).toBeFalsy();
});
});

View File

@ -16,6 +16,13 @@ const App: React.FC = () => {
</Button>
<TextArea rows={4} autoSize={autoResize} defaultValue={defaultValue} />
<TextArea allowClear style={{ width: 93 }} />
<br />
<TextArea
style={{
resize: 'both',
}}
showCount
/>
</>
);
};

View File

@ -1,34 +0,0 @@
import React from 'react';
import type { TextAreaRef } from 'rc-textarea';
import raf from 'rc-util/lib/raf';
type ResizeWrapperHandler = (rcTextArea: TextAreaRef | null) => void;
const ELEMENT_GAP = 2;
const adjustElementWidth = (width: number, wrapper: HTMLElement): void => {
if (wrapper.offsetWidth - width < ELEMENT_GAP) {
// The textarea's width is increased
wrapper.style.width = `${width + ELEMENT_GAP}px`;
} else if (wrapper.offsetWidth - width > ELEMENT_GAP) {
// The textarea's width is decreased
wrapper.style.width = `${width + ELEMENT_GAP}px`;
}
};
const useHandleResizeWrapper = () => {
const handleResizeWrapper = React.useCallback<ResizeWrapperHandler>((rcTextArea) => {
if (!rcTextArea) {
return;
}
if (rcTextArea.resizableTextArea.textArea.style.width.includes('px')) {
const width = Number.parseInt(
rcTextArea.resizableTextArea.textArea.style.width.replace(/px/, ''),
);
raf(() => adjustElementWidth(width, rcTextArea.nativeElement));
}
}, []);
return handleResizeWrapper;
};
export default useHandleResizeWrapper;

View File

@ -70,17 +70,6 @@ export const genBasicInputStyle = (token: InputToken): CSSObject => ({
transition: `all ${token.motionDurationMid}`,
...genPlaceholderStyle(token.colorTextPlaceholder),
// Reset height for `textarea`s
'textarea&': {
maxWidth: '100%', // prevent textarea resize from coming out of its container
height: 'auto',
minHeight: token.controlHeight,
lineHeight: token.lineHeight,
verticalAlign: 'bottom',
transition: `all ${token.motionDurationSlow}, height 0s`,
resize: 'vertical',
},
// Size
'&-lg': {
...genInputLargeStyle(token),
@ -411,7 +400,7 @@ export const genInputStyle: GenerateStyle<InputToken> = (token: InputToken) => {
'&[type="search"]::-webkit-search-cancel-button, &[type="search"]::-webkit-search-decoration':
{
'-webkit-appearance': 'none',
appearance: 'none',
},
},
};

View File

@ -11,6 +11,26 @@ const genTextAreaStyle: GenerateStyle<InputToken> = (token) => {
const textareaPrefixCls = `${componentCls}-textarea`;
return {
// Raw Textarea
[`textarea${componentCls}`]: {
maxWidth: '100%', // prevent textarea resize from coming out of its container
height: 'auto',
minHeight: token.controlHeight,
lineHeight: token.lineHeight,
verticalAlign: 'bottom',
transition: `all ${token.motionDurationSlow}`,
resize: 'vertical',
[`&${componentCls}-mouse-active`]: {
transition: `all ${token.motionDurationSlow}, height 0s, width 0s`,
},
},
// Wrapper for resize
[`${componentCls}-textarea-affix-wrapper-resize-dirty`]: {
width: 'auto',
},
[textareaPrefixCls]: {
position: 'relative',

View File

@ -223,7 +223,7 @@ const genBaseFilledStyle = (
borderColor: 'transparent',
'input&, & input, textarea&, & textarea': {
color: options?.inputColor,
color: options?.inputColor ?? 'unset',
},
'&:hover': {

View File

@ -25,7 +25,7 @@ const localeValues: Locale = {
filterSearchPlaceholder: 'جستجو در فیلترها',
emptyText: 'بدون داده',
selectAll: 'انتخاب صفحه‌ی کنونی',
selectInvert: 'معکوس کردن انتخاب‌ها در صفحه ی کنونی',
selectInvert: 'معکوس کردن انتخاب‌ها در صفحهی کنونی',
selectNone: 'انتخاب هیچکدام',
selectionAll: 'انتخاب همه‌ی داده‌ها',
sortTitle: 'مرتب سازی',
@ -58,8 +58,9 @@ const localeValues: Locale = {
selectCurrent: 'انتخاب صفحه فعلی',
removeCurrent: 'پاک کردن انتخاب‌های صفحه فعلی',
selectAll: 'انتخاب همه',
deselectAll: 'لغو انتخاب همه',
removeAll: 'پاک کردن همه انتخاب‌ها',
selectInvert: 'معکوس کردن انتخاب‌ها در صفحه ی کنونی',
selectInvert: 'معکوس کردن انتخاب‌ها در صفحهی کنونی',
},
Upload: {
uploading: 'در حال آپلود...',
@ -79,6 +80,7 @@ const localeValues: Locale = {
copy: 'کپی',
copied: 'کپی شد',
expand: 'توسعه',
collapse: 'بستن',
},
Form: {
optional: '(اختیاری)',
@ -134,8 +136,15 @@ const localeValues: Locale = {
preview: 'پیش‌نمایش',
},
QRCode: {
expired: 'QR Code منقضی شذد',
expired: 'کد QR منقضی شد',
refresh: 'به‌روزرسانی',
scanned: 'اسکن شد',
},
ColorPicker: {
presetEmpty: 'خالی',
transparent: 'شفاف',
singleColor: 'تک‌رنگ',
gradientColor: 'گرادینت',
},
};

View File

@ -1,6 +1,11 @@
import { unit } from '@ant-design/cssinjs';
import { genBasicInputStyle, genPlaceholderStyle, initComponentToken, initInputToken } from '../../input/style';
import {
genBasicInputStyle,
genPlaceholderStyle,
initComponentToken,
initInputToken,
} from '../../input/style';
import type { SharedComponentToken, SharedInputToken } from '../../input/style/token';
import {
genBorderlessStyle,
@ -163,7 +168,7 @@ const genMentionsStyle: GenerateStyle<MentionsToken> = (token) => {
[`> textarea, ${componentCls}-measure`]: {
color: colorText,
boxSizing: 'border-box',
minHeight: token.calc(controlHeight).sub(2),
minHeight: token.calc(controlHeight).sub(2).equal(),
margin: 0,
padding: `${unit(paddingBlock)} ${unit(paddingInline)}`,
overflow: 'inherit',

View File

@ -213,7 +213,6 @@ export const genNoticeStyle = (token: NotificationToken): CSSObject => {
position: 'absolute',
display: 'block',
appearance: 'none',
WebkitAppearance: 'none',
inlineSize: `calc(100% - ${unit(borderRadiusLG)} * 2)`,
left: {
_skip_check_: true,

View File

@ -62,7 +62,7 @@ const getSearchInputWithoutBorderStyle: GenerateStyle<SelectToken, CSSObject> =
'&::-webkit-search-cancel-button': {
display: 'none',
'-webkit-appearance': 'none',
appearance: 'none',
},
},
};

View File

@ -419,7 +419,7 @@ export const prepareComponentToken: GetDefaultToken<'Steps'> = (token) => ({
dotSize: token.controlHeight / 4,
dotCurrentSize: token.controlHeightLG / 4,
navArrowColor: token.colorTextDisabled,
navContentMaxWidth: 'auto',
navContentMaxWidth: 'unset',
descriptionMaxWidth: 140,
waitIconColor: token.wireframe ? token.colorTextDisabled : token.colorTextLabel,
waitIconBgColor: token.wireframe ? token.colorBgContainer : token.colorFillContent,

View File

@ -48,7 +48,7 @@ const genStickyStyle: GenerateStyle<TableToken, CSSObject> = (token) => {
height: tableScrollThumbSize,
backgroundColor: tableScrollThumbBg,
borderRadius: stickyScrollBarBorderRadius,
transition: `all ${token.motionDurationSlow}, transform none`,
transition: `all ${token.motionDurationSlow}, transform 0s`,
position: 'absolute',
bottom: 0,

View File

@ -3,10 +3,10 @@ import { unit } from '@ant-design/cssinjs';
import { getStyle as getCheckboxStyle } from '../../checkbox/style';
import type {
AliasToken,
CSSUtil,
FullToken,
GenerateStyle,
GetDefaultToken,
CSSUtil,
} from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal';
import type { TreeSharedToken } from '../../tree/style';
@ -39,6 +39,7 @@ const genBaseStyle: GenerateStyle<TreeSelectToken> = (token) => {
mergeToken<AliasToken & TreeSharedToken & CSSUtil>(token, {
colorBgContainer: colorBgElevated,
}),
false, // No need style of directory tree
),
{
[treeCls]: {

View File

@ -432,6 +432,12 @@ export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject =>
export const genTreeStyle = (
prefixCls: string,
token: AliasToken & TreeSharedToken & CSSUtil,
/**
* @descCN
* @descEN Whether to enable directory style
* @default true
*/
enableDirectory = true,
): CSSInterpolation => {
const treeCls = `.${prefixCls}`;
const treeNodeCls = `${treeCls}-treenode`;
@ -448,8 +454,8 @@ export const genTreeStyle = (
// Basic
genBaseStyle(prefixCls, treeToken),
// Directory
genDirectoryStyle(treeToken),
];
enableDirectory && genDirectoryStyle(treeToken),
].filter(Boolean);
};
export const initComponentToken = (token: AliasToken): TreeSharedToken => {

View File

@ -220,6 +220,8 @@ export default () => (
### TailwindCSS Arrange `@layer`
Before starting the following configuration, you need to enable [`@layer`](#layer) feature.
#### TailwindCSS v3
In global.css, adjust `@layer` to control the order of style override. Place `tailwind-base` before `antd`:

View File

@ -220,6 +220,8 @@ export default () => (
### TailwindCSS 排布 `@layer`
在开始以下配置前,你需要先启用 [`@layer`](#layer-样式优先级降权) 功能。
#### TailwindCSS v3
在 global.css 中,调整 `@layer` 来控制样式的覆盖顺序。让 `tailwind-base` 置于 `antd` 之前:

View File

@ -192,6 +192,7 @@
"@types/adm-zip": "^0.5.6",
"@types/ali-oss": "^6.16.11",
"@types/cli-progress": "^3.11.6",
"@types/css-tree": "^2.3.10",
"@types/fs-extra": "^11.0.4",
"@types/gtag.js": "^0.0.20",
"@types/http-server": "^0.12.4",
@ -232,11 +233,13 @@
"cli-progress": "^3.12.0",
"cross-env": "^7.0.3",
"cross-fetch": "^4.0.0",
"css-tree": "^3.1.0",
"csstree-validator": "^4.0.1",
"cypress-image-diff-html-report": "2.2.0",
"dekko": "^0.2.1",
"dotenv": "^16.4.5",
"dumi": "~2.4.17",
"dumi-plugin-color-chunk": "^1.1.2",
"dumi-plugin-color-chunk": "^2.1.0",
"env-paths": "^3.0.0",
"eslint": "^9.15.0",
"eslint-plugin-compat": "^6.0.1",

View File

@ -1,5 +1,8 @@
import path from 'path';
import React from 'react';
import {
createCache,
extractStyle,
legacyNotSelectorLinter,
logicalPropertiesLinter,
NaNLinter,
@ -7,11 +10,19 @@ import {
StyleProvider,
} from '@ant-design/cssinjs';
import chalk from 'chalk';
import { parse } from 'css-tree';
import type { SyntaxParseError } from 'css-tree';
import { validate } from 'csstree-validator';
import fs from 'fs-extra';
import isCI from 'is-ci';
import ReactDOMServer from 'react-dom/server';
import { ConfigProvider } from '../components';
import { generateCssinjs } from './generate-cssinjs';
const tmpDir = path.join(`${__filename}.tmp`);
fs.emptyDirSync(tmpDir);
console.log(chalk.green(`🔥 Checking CSS-in-JS...`));
let errorCount = 0;
@ -25,6 +36,20 @@ console.error = (msg: any) => {
}
};
// https://github.com/csstree/validator/blob/7df8ca/lib/validate.js#L187
function cssValidate(css: string, filename: string) {
const errors: SyntaxParseError[] = [];
const ast = parse(css, {
filename,
positions: true,
onParseError(error) {
errors.push(error);
},
});
return errors.concat(validate(ast));
}
async function checkCSSVar() {
await generateCssinjs({
key: 'check',
@ -39,6 +64,38 @@ async function checkCSSVar() {
},
});
}
async function checkCSSContent() {
const errors = new Map();
await generateCssinjs({
key: 'css-validate',
render(Component: any, filePath: string) {
const cache = createCache();
ReactDOMServer.renderToString(
<StyleProvider cache={cache}>
<Component />
</StyleProvider>,
);
const css = extractStyle(cache, { types: 'style', plain: true });
let showPath = filePath;
if (!isCI) {
const [, name] = filePath.split(path.sep);
const writeLocalPath = path.join(tmpDir, `${name}.css`);
showPath = path.relative(process.cwd(), writeLocalPath);
fs.writeFileSync(writeLocalPath, `/* ${filePath} */\n${css}`);
}
errors.set(filePath, cssValidate(css, showPath));
},
});
for (const [filePath, error] of errors) {
if (error.length > 0) {
errorCount += error.length;
console.log(chalk.red(`${filePath} has ${error.length} errors:`));
console.log(error);
}
}
}
(async () => {
await generateCssinjs({
@ -55,6 +112,7 @@ async function checkCSSVar() {
});
await checkCSSVar();
await checkCSSContent();
if (errorCount > 0) {
console.log(chalk.red(`❌ CSS-in-JS check failed with ${errorCount} errors.`));

View File

@ -7,7 +7,7 @@ type StyleFn = (prefix?: string) => void;
interface GenCssinjsOptions {
key: string;
render: (component: React.FC) => void;
render: (Component: React.FC, filepath: string) => void;
beforeRender?: (componentName: string) => void;
}
@ -35,6 +35,10 @@ export const generateCssinjs = ({ key, beforeRender, render }: GenCssinjsOptions
useRowStyle(prefixCls);
useColStyle(prefixCls);
};
} else if (file.includes('tree-select')) {
const originalUseStyle = (await import(absPath)).default;
useStyle = (prefixCls, treePrefixCls = `${prefixCls}-tree`) =>
originalUseStyle(prefixCls, treePrefixCls);
} else {
useStyle = (await import(absPath)).default;
}
@ -43,6 +47,6 @@ export const generateCssinjs = ({ key, beforeRender, render }: GenCssinjsOptions
return React.createElement('div');
};
beforeRender?.(componentName);
render?.(Demo);
render?.(Demo, path.relative(process.cwd(), file));
}),
);

View File

@ -29,3 +29,5 @@ declare module '@npmcli/run-script' {
declare module '@microflash/rehype-figure';
declare module 'dekko';
declare module 'csstree-validator';