mirror of
https://github.com/ant-design/ant-design.git
synced 2026-02-10 11:29:19 +08:00
Compare commits
300 Commits
5.26.6
...
chore/scri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d85e0e0163 | ||
|
|
2818ba6b87 | ||
|
|
17cdff8c4c | ||
|
|
9b953b0fcc | ||
|
|
9823f348fc | ||
|
|
bd00e59fc1 | ||
|
|
dc179c8985 | ||
|
|
fee3e97a26 | ||
|
|
7d079c2560 | ||
|
|
809b8820f9 | ||
|
|
6abbee7273 | ||
|
|
2327d5bca9 | ||
|
|
2ad2ef7bfd | ||
|
|
c4b6a30e8f | ||
|
|
669e948638 | ||
|
|
54afae7767 | ||
|
|
da79662278 | ||
|
|
aa13ad28e4 | ||
|
|
e9622c2d26 | ||
|
|
9c129ac7b1 | ||
|
|
f47947d857 | ||
|
|
bb21a2dfa6 | ||
|
|
620d1bf431 | ||
|
|
2abbbda693 | ||
|
|
e714dde0dc | ||
|
|
95bca1cf70 | ||
|
|
76392a2fc0 | ||
|
|
dde07ee096 | ||
|
|
129b4d6e6f | ||
|
|
ac515bc0a6 | ||
|
|
017fea5bb4 | ||
|
|
117673d618 | ||
|
|
72f26b3898 | ||
|
|
6047298565 | ||
|
|
63e2dfd682 | ||
|
|
7a520404aa | ||
|
|
a07161fa05 | ||
|
|
be85401160 | ||
|
|
dc362e2e1c | ||
|
|
eca2da28c9 | ||
|
|
a4bb8aef9a | ||
|
|
1b6d5527c6 | ||
|
|
450c2b2cbb | ||
|
|
91a4451c5b | ||
|
|
3dad1bd09e | ||
|
|
44cfd0cdeb | ||
|
|
b8cb35ca2d | ||
|
|
a1a6f1bfff | ||
|
|
ef45e96227 | ||
|
|
6d703575ff | ||
|
|
378fcad5e1 | ||
|
|
70a9d43393 | ||
|
|
1370c11cf7 | ||
|
|
9ac07bea68 | ||
|
|
a2c571ff00 | ||
|
|
af89759d8d | ||
|
|
fce28d79af | ||
|
|
3fcd6c8573 | ||
|
|
004eab7736 | ||
|
|
7833e84c8d | ||
|
|
6b8d833cad | ||
|
|
ec57a0eb02 | ||
|
|
3635c80085 | ||
|
|
85812ed060 | ||
|
|
b5d72b5a77 | ||
|
|
c060c55fc4 | ||
|
|
7c5ae3b168 | ||
|
|
9f90f35513 | ||
|
|
3e88285d77 | ||
|
|
87601689fa | ||
|
|
5db1e6f449 | ||
|
|
90b4a4373d | ||
|
|
8a7e15460c | ||
|
|
2589e693cf | ||
|
|
94d5b800a7 | ||
|
|
dfc34f6ae3 | ||
|
|
0b5ef77e5d | ||
|
|
4d15a1715d | ||
|
|
1d433d59d5 | ||
|
|
bd1aa0114e | ||
|
|
647eba0302 | ||
|
|
6ca1cbed80 | ||
|
|
b59a1102f1 | ||
|
|
00b24f5be7 | ||
|
|
625e034a32 | ||
|
|
b138cadf40 | ||
|
|
cb0dd956ba | ||
|
|
594ee4d989 | ||
|
|
f1036be04f | ||
|
|
030783f41b | ||
|
|
9d07df9667 | ||
|
|
b189e0ec97 | ||
|
|
6562c20134 | ||
|
|
74c26d895d | ||
|
|
18e596b1f3 | ||
|
|
e3e7db9fd1 | ||
|
|
4d86c53505 | ||
|
|
fb7bbbea50 | ||
|
|
531ea91296 | ||
|
|
e716aec310 | ||
|
|
55a628edf8 | ||
|
|
90cacd5e45 | ||
|
|
c172411eb4 | ||
|
|
2e8f162ff7 | ||
|
|
209f4e88d0 | ||
|
|
4bf3f72e09 | ||
|
|
6e72e7bfbe | ||
|
|
21e88f1f05 | ||
|
|
9daba47ce9 | ||
|
|
5e684e8e47 | ||
|
|
569ef8025c | ||
|
|
2a2bd3c0a3 | ||
|
|
181e327976 | ||
|
|
1c4138d170 | ||
|
|
9ba69f0010 | ||
|
|
1a2574aaeb | ||
|
|
8af8de294b | ||
|
|
c88004aca9 | ||
|
|
31fdae68d2 | ||
|
|
dbbdaf394c | ||
|
|
2e9842cae6 | ||
|
|
4bf930c5a5 | ||
|
|
eec3e36eb9 | ||
|
|
a703957ce3 | ||
|
|
69f3664cfe | ||
|
|
1b76227780 | ||
|
|
637fc8069e | ||
|
|
7c68d79224 | ||
|
|
7599bbe319 | ||
|
|
fffc12122d | ||
|
|
8065093543 | ||
|
|
c1a6a87285 | ||
|
|
9628555adc | ||
|
|
750934d3e0 | ||
|
|
6119e626c8 | ||
|
|
47f596cf1c | ||
|
|
d40799ad4b | ||
|
|
b2ad4f59a7 | ||
|
|
b76826f426 | ||
|
|
77d02d19d6 | ||
|
|
4d295e20db | ||
|
|
71b53a0db3 | ||
|
|
78e015b313 | ||
|
|
23a2b5ec96 | ||
|
|
1563f59f1b | ||
|
|
a1836c4ec6 | ||
|
|
d6f26ee09f | ||
|
|
4d4621a03f | ||
|
|
38095a7641 | ||
|
|
7dc77c9087 | ||
|
|
507985619f | ||
|
|
88e46f6993 | ||
|
|
2b1e677ae7 | ||
|
|
47307bec98 | ||
|
|
05a35a6919 | ||
|
|
2ba4e87fca | ||
|
|
754d8bb058 | ||
|
|
79c79e9109 | ||
|
|
95730ba90f | ||
|
|
23b6737ad7 | ||
|
|
465567f66d | ||
|
|
8e54fbd4fe | ||
|
|
5b6cc4c987 | ||
|
|
c20fae6695 | ||
|
|
95ade639c9 | ||
|
|
2a66972cf9 | ||
|
|
2b7a87b14e | ||
|
|
1ab5e38e70 | ||
|
|
104394c740 | ||
|
|
ddd525c789 | ||
|
|
c453b01a4b | ||
|
|
33d17cd831 | ||
|
|
63a1dc3e77 | ||
|
|
07de78d3cf | ||
|
|
8c9bf94dd8 | ||
|
|
08f2fc31ad | ||
|
|
178d7c721a | ||
|
|
7b930a2fe8 | ||
|
|
90df266491 | ||
|
|
a267f79f08 | ||
|
|
0653add28f | ||
|
|
40a0f30b6f | ||
|
|
ce2b4fd50c | ||
|
|
0aa7561b00 | ||
|
|
57d7571b29 | ||
|
|
5e7f63fb30 | ||
|
|
ba42d6842d | ||
|
|
7db17b3eaa | ||
|
|
33691cc525 | ||
|
|
c6b1ad0851 | ||
|
|
eecf04371a | ||
|
|
9be96b92a7 | ||
|
|
5c61993356 | ||
|
|
b7c76d0a46 | ||
|
|
52a701b497 | ||
|
|
ca4882e0f2 | ||
|
|
ed0a250805 | ||
|
|
570efda7eb | ||
|
|
1f229dc7b8 | ||
|
|
89bbc56213 | ||
|
|
c0b427e47f | ||
|
|
ed6d440437 | ||
|
|
c2ec447866 | ||
|
|
2cc9103149 | ||
|
|
70f79ec0ad | ||
|
|
613aad6a5d | ||
|
|
c95a08e612 | ||
|
|
7cea3bb0b4 | ||
|
|
3fbcd6d080 | ||
|
|
6de357d8f9 | ||
|
|
feaed51f09 | ||
|
|
0b5a36f330 | ||
|
|
e59c2a77f7 | ||
|
|
3e3897e09e | ||
|
|
9d46bd7e45 | ||
|
|
2a30698f14 | ||
|
|
2597ab6b97 | ||
|
|
81a86966d2 | ||
|
|
5f75711a80 | ||
|
|
f5f8967148 | ||
|
|
9a5d620dc5 | ||
|
|
46c9c93773 | ||
|
|
3f94d76ba9 | ||
|
|
8d36927449 | ||
|
|
c2e4c0dc47 | ||
|
|
7c9a8fabee | ||
|
|
618fac974b | ||
|
|
e82a7fae8c | ||
|
|
bb8187416b | ||
|
|
260016c2cc | ||
|
|
4eab7c7140 | ||
|
|
06306cd37f | ||
|
|
86385a2f4d | ||
|
|
416c76997f | ||
|
|
953a8c0278 | ||
|
|
ff5218840f | ||
|
|
be8b735b46 | ||
|
|
3403561694 | ||
|
|
d9f8a84062 | ||
|
|
48eb543646 | ||
|
|
d56b3ec554 | ||
|
|
44369acc72 | ||
|
|
b0a3a5b485 | ||
|
|
4d1a6aff98 | ||
|
|
36c93d567b | ||
|
|
bb61567776 | ||
|
|
3d6eaabc26 | ||
|
|
7f47601b88 | ||
|
|
36b706bdc8 | ||
|
|
a15aa44c4a | ||
|
|
b06330f8c8 | ||
|
|
4138850801 | ||
|
|
70b9f45404 | ||
|
|
f71752f0a7 | ||
|
|
31c37208d5 | ||
|
|
a614e8b45e | ||
|
|
288fe13425 | ||
|
|
1cf55b567b | ||
|
|
4022993143 | ||
|
|
b883612670 | ||
|
|
e20ca2c169 | ||
|
|
0109f3f186 | ||
|
|
2d0f484700 | ||
|
|
4d3ff76a54 | ||
|
|
ca06fb41c5 | ||
|
|
bc66f1d18c | ||
|
|
b3ae5670e7 | ||
|
|
8379c40bc1 | ||
|
|
c02424d21b | ||
|
|
74b3270f6d | ||
|
|
30fd3fd209 | ||
|
|
596ebb01d0 | ||
|
|
ff2c15e410 | ||
|
|
a23b2c082f | ||
|
|
3abd63c432 | ||
|
|
eee7784a63 | ||
|
|
2f0e80ccb8 | ||
|
|
ec40a40dc0 | ||
|
|
13555bfa4f | ||
|
|
2c38b8430a | ||
|
|
61ebc35d53 | ||
|
|
cc92210f63 | ||
|
|
402a9a36cd | ||
|
|
06ef05d7da | ||
|
|
bf37b20527 | ||
|
|
6e9e879a68 | ||
|
|
8455ebbc1f | ||
|
|
0fba5f81a8 | ||
|
|
a6b5c0f28c | ||
|
|
893507f87e | ||
|
|
fb5be0e723 | ||
|
|
6026f9df65 | ||
|
|
d5f5e9663c | ||
|
|
b2be2f38b4 | ||
|
|
ff32ecbf65 | ||
|
|
1dfdcc2f4e | ||
|
|
c34e0b94be | ||
|
|
665369ea72 | ||
|
|
03f6fb0366 | ||
|
|
d3458848b4 |
@@ -1,80 +0,0 @@
|
||||
---
|
||||
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,及时更新到新推荐用法
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
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 定义
|
||||
- 提供常见问题解答
|
||||
- 更新文档时同步更新中英文版本
|
||||
@@ -1,128 +0,0 @@
|
||||
---
|
||||
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/xxx:Bug 修复
|
||||
- docs/xxx:文档更新
|
||||
- PR 说明中选择改动类型:
|
||||
- 🆕 新特性提交
|
||||
- 🐞 Bug 修复
|
||||
- 📝 文档改进
|
||||
- 📽️ 演示代码改进
|
||||
- 💄 样式/交互改进
|
||||
- 🤖 TypeScript 更新
|
||||
- 📦 包体积优化
|
||||
- ⚡️ 性能优化
|
||||
- 🌐 国际化改进
|
||||
- 提供改动背景和解决方案
|
||||
- 更新日志同时提供英文和中文版本
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
description: 本地化规范文档
|
||||
globs: ["components/locale/*_*.ts", "components/locale/index.tsx"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 本地化规范
|
||||
|
||||
antd 中所有的本地化配置都应该在 `components/locale` 目录中完成定义,主要分为两步:类型定义和本地化配置
|
||||
|
||||
## 类型定义
|
||||
|
||||
antd 的本地化配置的类型定义的入口文件是 `components/locale/index.tsx`, 当需要添加新的本地化配置时,需要检查对应组件或全局配置的类型是否存在,如果不存在,则需要增加相应的类型描述。
|
||||
如果新增或修改的本地化配置时组件配置,那么具体的本地化类型应该在相应的组件目录定义,定义好后在 `components/locale/index.tsx` 引入对应组件的类型定义。
|
||||
|
||||
## 本地化配置
|
||||
|
||||
### 纯字符串配置
|
||||
|
||||
antd 中的本地化配置文件命名规则是:`*_*.ts`,如:`zh_CN.ts`,文件默认导出一个 `Locale` 类型对象。
|
||||
通常在为 antd 添加后修改某一项本地化配置时,如无特殊说明,需要同时修改所有语言的本地化配置。
|
||||
|
||||
本地化配置文件列表如下(包括但不限于):
|
||||
|
||||
```json
|
||||
["components/locale/ar_EG.ts","components/locale/az_AZ.ts","components/locale/bg_BG.ts","components/locale/bn_BD.ts","components/locale/by_BY.ts","components/locale/ca_ES.ts","components/locale/cs_CZ.ts","components/locale/da_DK.ts","components/locale/de_DE.ts","components/locale/el_GR.ts","components/locale/en_GB.ts","components/locale/en_US.ts","components/locale/es_ES.ts","components/locale/et_EE.ts","components/locale/eu_ES.ts","components/locale/fa_IR.ts","components/locale/fi_FI.ts","components/locale/fr_BE.ts","components/locale/fr_CA.ts","components/locale/fr_FR.ts","components/locale/ga_IE.ts","components/locale/gl_ES.ts","components/locale/he_IL.ts","components/locale/hi_IN.ts","components/locale/hr_HR.ts","components/locale/hu_HU.ts","components/locale/hy_AM.ts","components/locale/id_ID.ts","components/locale/is_IS.ts","components/locale/it_IT.ts","components/locale/ja_JP.ts","components/locale/ka_GE.ts","components/locale/kk_KZ.ts","components/locale/km_KH.ts","components/locale/kmr_IQ.ts","components/locale/kn_IN.ts","components/locale/ko_KR.ts","components/locale/ku_IQ.ts","components/locale/lt_LT.ts","components/locale/lv_LV.ts","components/locale/mk_MK.ts","components/locale/ml_IN.ts","components/locale/mn_MN.ts","components/locale/ms_MY.ts","components/locale/my_MM.ts","components/locale/nb_NO.ts","components/locale/ne_NP.ts","components/locale/nl_BE.ts","components/locale/nl_NL.ts","components/locale/pl_PL.ts","components/locale/pt_BR.ts","components/locale/pt_PT.ts","components/locale/ro_RO.ts","components/locale/ru_RU.ts","components/locale/si_LK.ts","components/locale/sk_SK.ts","components/locale/sl_SI.ts","components/locale/sr_RS.ts","components/locale/sv_SE.ts","components/locale/ta_IN.ts","components/locale/th_TH.ts","components/locale/tk_TK.ts","components/locale/tr_TR.ts","components/locale/uk_UA.ts","components/locale/ur_PK.ts","components/locale/uz_UZ.ts","components/locale/vi_VN.ts","components/locale/zh_CN.ts","components/locale/zh_HK.ts","components/locale/zh_TW.ts"]
|
||||
```
|
||||
|
||||
本地化配置的内容通常是纯字符串,如:
|
||||
|
||||
```typescript
|
||||
{
|
||||
// ...
|
||||
Modal: {
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
justOkText: '知道了',
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 字符串模版配置
|
||||
|
||||
当然,也有一些需要配置需要再运行时实时替换变量的模版配置,带有 `${}` 的变量将在实际使用的地方被实时替换成对应的变量内容:
|
||||
```typescript
|
||||
{
|
||||
// ...
|
||||
date: {
|
||||
format: '${label}日期格式无效',
|
||||
parse: '${label}不能转换为日期',
|
||||
invalid: '${label}是一个无效日期',
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 全局配置
|
||||
|
||||
如果某个本地化配置不独属于某个组件,而是数据全局的本地化配置,此时应该在 `global` 中添加相关属性,如:
|
||||
|
||||
```typescript
|
||||
{
|
||||
// ...
|
||||
// locales for all components
|
||||
global: {
|
||||
placeholder: '请选择',
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
# 使用本地化
|
||||
|
||||
antd 中具体使用本地化配置时,可以使用 `components/locale/index.tsx` 文件中导出的 `useLocale` 获取全局上下文中配置的本地化,并跟组件属性中传入的本地化配置合并后得到最完整的本地化配置,如:
|
||||
|
||||
```tsx
|
||||
import { useLocale } from "../locale";
|
||||
import enUS from '../locale/en_US';
|
||||
|
||||
export function TestComp(props) {
|
||||
const { locale: propLocale } = props;
|
||||
const [contextLocale] = useLocale("TestComp", enUs);
|
||||
|
||||
const locale = {...contextLocale, ...propLocale};
|
||||
|
||||
return (
|
||||
<div title={locale?.title}>
|
||||
{locale?.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -1,108 +0,0 @@
|
||||
---
|
||||
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. `destroyOnHidden`)
|
||||
* 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)
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# 项目背景
|
||||
|
||||
这是 ant-design/ant-design(antd)的源代码仓库,是一个 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 优化性能
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
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% 时的正常布局
|
||||
- 避免使用会导致闪烁的动画
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs: **/__tests__/**,**/*.test.tsx,**/*.test.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
# 测试规范
|
||||
|
||||
- 使用 Jest 和 React Testing Library 编写单元测试
|
||||
- 对 UI 组件使用快照测试 (Snapshot Testing)
|
||||
- 测试覆盖率要求 100%
|
||||
- 测试文件放在 __tests__ 目录,命名格式为:index.test.tsx 或 xxx.test.tsx
|
||||
@@ -1,81 +0,0 @@
|
||||
# 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 注释说明用途
|
||||
@@ -2,7 +2,7 @@
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||
{
|
||||
"name": "ant-design",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:2-22-bookworm",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:4-22-bookworm",
|
||||
"postCreateCommand": "pnpm install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export default class FetchCache {
|
||||
private cache: Map<string, PromiseLike<any>> = new Map();
|
||||
|
||||
get(key: string) {
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
set(key: string, value: PromiseLike<any>) {
|
||||
this.cache.set(key, value);
|
||||
}
|
||||
|
||||
promise<T>(key: string, promiseFn: () => PromiseLike<T>): PromiseLike<T> {
|
||||
const cached = this.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const promise = promiseFn();
|
||||
this.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import fetch from 'cross-fetch';
|
||||
|
||||
import FetchCache from './cache';
|
||||
|
||||
const cache = new FetchCache();
|
||||
|
||||
const useFetch = <T>(options: string | { request: () => PromiseLike<T>; key: string }) => {
|
||||
let request;
|
||||
let key;
|
||||
if (typeof options === 'string') {
|
||||
request = () => fetch(options).then((res) => res.json());
|
||||
key = options;
|
||||
} else {
|
||||
request = options.request;
|
||||
key = options.key;
|
||||
}
|
||||
return React.use<T>(cache.promise<T>(key, request));
|
||||
};
|
||||
|
||||
export default useFetch;
|
||||
65
.dumi/hooks/useIssueCount.ts
Normal file
65
.dumi/hooks/useIssueCount.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
|
||||
const isNumber = (value: any): value is number => {
|
||||
return typeof value === 'number' && !Number.isNaN(value);
|
||||
};
|
||||
|
||||
const fetcher = async (url: string): Promise<number> => {
|
||||
// eslint-disable-next-line compat/compat
|
||||
const res = await fetch(url, { headers: { Accept: 'application/vnd.github+json' } });
|
||||
const data = await res.json();
|
||||
const totalCount = isNumber(data?.total_count) ? data.total_count : 0;
|
||||
return totalCount;
|
||||
};
|
||||
|
||||
const swrConfig: SWRConfiguration<number, Error> = {
|
||||
revalidateOnReconnect: true, // 网络重新连接时重新请求
|
||||
dedupingInterval: 1000 * 60, // 1 分钟内重复 key 不会重新请求
|
||||
shouldRetryOnError: true, // 错误重试
|
||||
errorRetryCount: 3, // 最多重试 3 次
|
||||
};
|
||||
|
||||
export interface UseIssueCountOptions {
|
||||
repo: string; // e.g. ant-design/ant-design
|
||||
proxyEndpoint?: string; // backend proxy endpoint to avoid GitHub rate limit
|
||||
titleKeywords?: string[]; // keywords to match in issue title
|
||||
}
|
||||
|
||||
export const useIssueCount = (options: UseIssueCountOptions) => {
|
||||
const { repo, proxyEndpoint, titleKeywords } = options;
|
||||
|
||||
// Note: current query only filters by title keywords. Filtering by component name can be added later if needed.
|
||||
const searchUrl = useMemo(() => {
|
||||
const tokens = (titleKeywords || []).filter(Boolean).map((k) => encodeURIComponent(String(k)));
|
||||
const orExpr = tokens.length > 0 ? tokens.join('%20OR%20') : '';
|
||||
const titlePart = orExpr ? `in:title+(${orExpr})` : 'in:title';
|
||||
const q = `repo:${repo}+is:issue+is:open+${titlePart}`;
|
||||
return `https://api.github.com/search/issues?q=${q}`;
|
||||
}, [repo, titleKeywords]);
|
||||
|
||||
const endpoint = proxyEndpoint || searchUrl;
|
||||
|
||||
const { data, error, isLoading } = useSWR<number, Error>(endpoint || null, fetcher, swrConfig);
|
||||
|
||||
const issueNewUrl = `https://github.com/${repo}/issues/new/choose`;
|
||||
|
||||
const issueSearchUrl = useMemo(() => {
|
||||
const keywords = (titleKeywords || []).filter(Boolean).map((k) => String(k));
|
||||
const groupExpr =
|
||||
keywords.length > 0 ? `(${keywords.map((k) => `is:issue in:title ${k}`).join(' OR ')})` : '';
|
||||
const qRaw = `is:open ${groupExpr}`.trim();
|
||||
return `https://github.com/${repo}/issues?q=${encodeURIComponent(qRaw)}`;
|
||||
}, [repo, titleKeywords]);
|
||||
|
||||
return {
|
||||
issueCount: data,
|
||||
issueCountError: error,
|
||||
issueCountLoading: isLoading,
|
||||
issueNewUrl,
|
||||
issueSearchUrl,
|
||||
};
|
||||
};
|
||||
|
||||
export default useIssueCount;
|
||||
@@ -221,7 +221,7 @@ const useMenu = (options: UseMenuOptions = {}): readonly [MenuProps['items'], st
|
||||
return result;
|
||||
}, []) ?? []
|
||||
);
|
||||
}, [sidebarData, fullData, pathname, search, options]);
|
||||
}, [sidebarData, pathname, fullData, search, before, after]);
|
||||
|
||||
return [menuItems, pathname] as const;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ const NotFoundPage: React.FC<NotFoundProps> = ({ router }) => {
|
||||
code: 11,
|
||||
msg: `Page not found: ${location.href}; Source: ${document.referrer}`,
|
||||
});
|
||||
}, []);
|
||||
}, [isZhCN, pathname, router]);
|
||||
|
||||
return (
|
||||
<Result
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Badge, Carousel, Flex, Skeleton, Typography } from 'antd';
|
||||
import { Alert, Badge, Carousel, Flex, Skeleton, Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import SiteContext from '../../../theme/slots/SiteContext';
|
||||
import type { Extra, Icon } from './util';
|
||||
import { getCarouselStyle, useSiteData } from './util';
|
||||
import { getCarouselStyle, useAntdSiteConfig } from './util';
|
||||
|
||||
const useStyle = createStyles(({ token, css, cx }) => {
|
||||
const { carousel } = getCarouselStyle();
|
||||
@@ -67,17 +67,20 @@ const useStyle = createStyles(({ token, css, cx }) => {
|
||||
interface RecommendItemProps {
|
||||
extra: Extra;
|
||||
index: number;
|
||||
icons: Icon[];
|
||||
icons?: Icon[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const RecommendItem: React.FC<RecommendItemProps> = ({ extra, index, icons, className }) => {
|
||||
const RecommendItem: React.FC<RecommendItemProps> = (props) => {
|
||||
const { extra, index, icons, className } = props;
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
if (!extra) {
|
||||
return <Skeleton key={index} />;
|
||||
}
|
||||
const icon = icons.find((i) => i.name === extra.source);
|
||||
|
||||
const icon = icons?.find((i) => i.name === extra.source);
|
||||
|
||||
const card = (
|
||||
<a
|
||||
@@ -93,7 +96,9 @@ const RecommendItem: React.FC<RecommendItemProps> = ({ extra, index, icons, clas
|
||||
</Typography.Paragraph>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Typography.Text>{extra.date}</Typography.Text>
|
||||
{icon && <img src={icon.href} draggable={false} className={styles.bannerBg} alt="banner" />}
|
||||
{icon?.href && (
|
||||
<img src={icon.href} draggable={false} className={styles.bannerBg} alt="banner" />
|
||||
)}
|
||||
</Flex>
|
||||
</a>
|
||||
);
|
||||
@@ -111,6 +116,7 @@ const RecommendItem: React.FC<RecommendItemProps> = ({ extra, index, icons, clas
|
||||
|
||||
export const BannerRecommendsFallback: React.FC = () => {
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
|
||||
const { styles } = useStyle();
|
||||
|
||||
const list = Array.from({ length: 3 });
|
||||
@@ -118,7 +124,7 @@ export const BannerRecommendsFallback: React.FC = () => {
|
||||
return isMobile ? (
|
||||
<Carousel className={styles.carousel}>
|
||||
{list.map((_, index) => (
|
||||
<div key={index} className={styles.itemBase}>
|
||||
<div key={`mobile-${index}`} className={styles.itemBase}>
|
||||
<Skeleton active style={{ padding: '0 24px' }} />
|
||||
</div>
|
||||
))}
|
||||
@@ -126,7 +132,7 @@ export const BannerRecommendsFallback: React.FC = () => {
|
||||
) : (
|
||||
<div className={styles.container}>
|
||||
{list.map((_, index) => (
|
||||
<div key={index} className={styles.itemBase}>
|
||||
<div key={`desktop-${index}`} className={styles.itemBase}>
|
||||
<Skeleton active />
|
||||
</div>
|
||||
))}
|
||||
@@ -138,25 +144,37 @@ const BannerRecommends: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
const [, lang] = useLocale();
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
const data = useSiteData();
|
||||
const extras = data?.extras?.[lang];
|
||||
const icons = data?.icons || [];
|
||||
const first3 =
|
||||
!extras || extras.length === 0 ? Array.from<any>({ length: 3 }) : extras.slice(0, 3);
|
||||
const { data, error, isLoading } = useAntdSiteConfig();
|
||||
|
||||
if (!data) {
|
||||
if (isLoading) {
|
||||
return <BannerRecommendsFallback />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert
|
||||
showIcon
|
||||
type="error"
|
||||
message={error.message}
|
||||
description={process.env.NODE_ENV !== 'production' ? error.stack : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const extras = data?.extras?.[lang];
|
||||
|
||||
const mergedExtras =
|
||||
!extras || !extras.length ? Array.from<Extra>({ length: 3 }) : extras.slice(0, 3);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Carousel className={styles.carousel}>
|
||||
{first3.map((extra, index) => (
|
||||
<div key={index}>
|
||||
{mergedExtras.map((extra, index) => (
|
||||
<div key={`mobile-${index}`}>
|
||||
<RecommendItem
|
||||
extra={extra}
|
||||
index={index}
|
||||
icons={icons}
|
||||
icons={data?.icons}
|
||||
className={styles.sliderItem}
|
||||
/>
|
||||
</div>
|
||||
@@ -167,13 +185,13 @@ const BannerRecommends: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{first3.map((extra, index) => (
|
||||
{mergedExtras.map((extra, index) => (
|
||||
<RecommendItem
|
||||
key={`desktop-${index}`}
|
||||
extra={extra}
|
||||
index={index}
|
||||
icons={icons}
|
||||
icons={data?.icons}
|
||||
className={styles.cardItem}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -258,7 +258,18 @@ const ComponentsList: React.FC = () => {
|
||||
),
|
||||
},
|
||||
],
|
||||
[isMobile],
|
||||
[
|
||||
isMobile,
|
||||
locale.inProgress,
|
||||
locale.lastMonth,
|
||||
locale.lastWeek,
|
||||
locale.lastYear,
|
||||
locale.sampleContent,
|
||||
locale.success,
|
||||
locale.taskFailed,
|
||||
locale.tour,
|
||||
locale.yesterday,
|
||||
],
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
|
||||
@@ -11,6 +11,14 @@ const useStyle = createStyles(({ css, token }) => ({
|
||||
position: relative;
|
||||
transition: all ${token.motionDurationSlow};
|
||||
`,
|
||||
container: css`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
`,
|
||||
typographyWrapper: css`
|
||||
text-align: center;
|
||||
`,
|
||||
marginStyle: css`
|
||||
max-width: 1208px;
|
||||
margin-inline: auto;
|
||||
@@ -40,40 +48,36 @@ const Group: React.FC<React.PropsWithChildren<GroupProps>> = (props) => {
|
||||
const token = useTheme();
|
||||
const { styles } = useStyle();
|
||||
const { isMobile } = React.use(SiteContext);
|
||||
const childNode = (
|
||||
<>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Title
|
||||
id={id}
|
||||
level={1}
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
color: titleColor,
|
||||
// Special for the title
|
||||
fontSize: isMobile ? token.fontSizeHeading2 : token.fontSizeHeading1,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
color: titleColor,
|
||||
marginBottom: isMobile ? token.marginXXL : token.marginFarXS,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<div className={classNames({ [styles.marginStyle]: !collapse })}>
|
||||
{children ? <div>{children}</div> : <div className={styles.withoutChildren} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: background }} className={styles.box}>
|
||||
<div style={{ position: 'absolute', inset: 0 }}>{decoration}</div>
|
||||
<GroupMaskLayer style={{ paddingBlock: token.marginFarSM }}>{childNode}</GroupMaskLayer>
|
||||
<div className={styles.container}>{decoration}</div>
|
||||
<GroupMaskLayer style={{ paddingBlock: token.marginFarSM }}>
|
||||
<div className={styles.typographyWrapper}>
|
||||
<Typography.Title
|
||||
id={id}
|
||||
level={1}
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
color: titleColor,
|
||||
// Special for the title
|
||||
fontSize: isMobile ? token.fontSizeHeading2 : token.fontSizeHeading1,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
color: titleColor,
|
||||
marginBottom: isMobile ? token.marginXXL : token.marginFarXS,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<div className={classNames({ [styles.marginStyle]: !collapse })}>
|
||||
{children ? <div>{children}</div> : <div className={styles.withoutChildren} />}
|
||||
</div>
|
||||
</GroupMaskLayer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -73,13 +73,12 @@ const ThemeColorPicker: React.FC<ThemeColorPickerProps> = ({ value, onChange, id
|
||||
|
||||
const matchColors = React.useMemo(() => {
|
||||
const valueStr = generateColor(value || '').toRgbString();
|
||||
let existActive = false;
|
||||
const colors = PRESET_COLORS.map((color) => {
|
||||
const colorStr = generateColor(color).toRgbString();
|
||||
const active = colorStr === valueStr;
|
||||
existActive = existActive || active;
|
||||
return { color, active, picker: false };
|
||||
return { color, active, picker: false } as const;
|
||||
});
|
||||
const existActive = colors.some((c) => c.active);
|
||||
|
||||
return [
|
||||
...colors,
|
||||
|
||||
@@ -93,7 +93,7 @@ const ThemePicker: React.FC<ThemePickerProps> = (props) => {
|
||||
})}
|
||||
>
|
||||
<input type="radio" name="theme" id={index === 0 ? id : undefined} />
|
||||
<img src={THEMES[theme]} alt={theme} />
|
||||
<img draggable={false} src={THEMES[theme]} alt={theme} />
|
||||
</label>
|
||||
<span>{locale[theme]}</span>
|
||||
</Flex>
|
||||
|
||||
@@ -471,6 +471,7 @@ const Theme: React.FC = () => {
|
||||
<div className={styles.logo}>
|
||||
<div className={styles.logoImg}>
|
||||
<img
|
||||
draggable={false}
|
||||
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
|
||||
style={{
|
||||
filter:
|
||||
@@ -500,7 +501,7 @@ const Theme: React.FC = () => {
|
||||
className={classNames(styles.transBg)}
|
||||
selectedKeys={['Themes']}
|
||||
openKeys={['Design']}
|
||||
style={{ height: '100%', borderRight: 0 }}
|
||||
style={{ height: '100%', borderInlineEnd: 0 }}
|
||||
items={sideMenuItems}
|
||||
expandIcon={false}
|
||||
/>
|
||||
@@ -588,15 +589,17 @@ const Theme: React.FC = () => {
|
||||
>
|
||||
{/* Image Left Top */}
|
||||
<img
|
||||
draggable={false}
|
||||
className={classNames(styles.pos, styles.leftTopImage)}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/bd71b0c6-f93a-4e52-9c8a-f01a9b8fe22b.svg"
|
||||
alt=""
|
||||
alt="image-left-top"
|
||||
/>
|
||||
{/* Image Right Bottom */}
|
||||
<img
|
||||
draggable={false}
|
||||
className={classNames(styles.pos, styles.rightBottomImage)}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/84ad805a-74cb-4916-b7ba-9cdc2bdec23a.svg"
|
||||
alt=""
|
||||
alt="image-right-bottom"
|
||||
/>
|
||||
</div>
|
||||
{/* >>>>>> Dark <<<<<< */}
|
||||
@@ -608,15 +611,17 @@ const Theme: React.FC = () => {
|
||||
>
|
||||
{/* Image Left Top */}
|
||||
<img
|
||||
draggable={false}
|
||||
className={classNames(styles.pos, styles.leftTopImagePos)}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/a213184a-f212-4afb-beec-1e8b36bb4b8a.svg"
|
||||
alt=""
|
||||
alt="image-left-top"
|
||||
/>
|
||||
{/* Image Right Bottom */}
|
||||
<img
|
||||
draggable={false}
|
||||
className={classNames(styles.pos, styles.rightBottomPos)}
|
||||
src="https://gw.alipayobjects.com/zos/bmw-prod/bb74a2fb-bff1-4d0d-8c2d-2ade0cd9bb0d.svg"
|
||||
alt=""
|
||||
alt="image-right-bottom"
|
||||
/>
|
||||
</div>
|
||||
{/* >>>>>> Background Image <<<<<< */}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
/* eslint-disable compat/compat */
|
||||
import { css } from 'antd-style';
|
||||
import fetch from 'cross-fetch';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export interface Author {
|
||||
avatar: string;
|
||||
@@ -82,19 +82,13 @@ export function preLoad(list: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
export function useSiteData(): Partial<SiteData> | undefined {
|
||||
const [data, setData] = useState<SiteData | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://render.alipay.com/p/h5data/antd4-config_website-h5data.json').then(
|
||||
async (res) => {
|
||||
setData(await res.json());
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
return data;
|
||||
}
|
||||
export const useAntdSiteConfig = () => {
|
||||
const { data, error, isLoading } = useSWR<Partial<SiteData>, Error>(
|
||||
`https://render.alipay.com/p/h5data/antd4-config_website-h5data.json`,
|
||||
(url: string) => fetch(url).then((res) => res.json()),
|
||||
);
|
||||
return { data, error, isLoading };
|
||||
};
|
||||
|
||||
export const getCarouselStyle = () => ({
|
||||
carousel: css`
|
||||
|
||||
@@ -51,11 +51,7 @@ const Homepage: React.FC = () => {
|
||||
|
||||
<div>
|
||||
{/* 定制主题 */}
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
}}
|
||||
>
|
||||
<ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
|
||||
<Suspense fallback={null}>
|
||||
<Theme />
|
||||
</Suspense>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Suspense, useEffect } from 'react';
|
||||
import { App, Button, Skeleton } from 'antd';
|
||||
import { App, Button, ConfigProvider, Skeleton } from 'antd';
|
||||
import { enUS, zhCN } from 'antd-token-previewer';
|
||||
import type { ThemeConfig } from 'antd/es/config-provider/context';
|
||||
import { Helmet } from 'dumi';
|
||||
@@ -61,21 +61,23 @@ const CustomTheme: React.FC = () => {
|
||||
<meta property="og:title" content={`${locale.title} - Ant Design`} />
|
||||
</Helmet>
|
||||
<Suspense fallback={<Skeleton style={{ margin: 24 }} />}>
|
||||
<ThemeEditor
|
||||
advanced
|
||||
hideAdvancedSwitcher
|
||||
theme={{ name: 'Custom Theme', key: 'test', config: theme }}
|
||||
style={{ height: 'calc(100vh - 64px)' }}
|
||||
onThemeChange={(newTheme) => {
|
||||
setTheme(newTheme.config);
|
||||
}}
|
||||
locale={lang === 'cn' ? zhCN : enUS}
|
||||
actions={
|
||||
<Button type="primary" onClick={handleSave}>
|
||||
{locale.save}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<ConfigProvider theme={{ inherit: false }}>
|
||||
<ThemeEditor
|
||||
advanced
|
||||
hideAdvancedSwitcher
|
||||
theme={{ name: 'Custom Theme', key: 'test', config: theme }}
|
||||
style={{ height: 'calc(100vh - 64px)' }}
|
||||
onThemeChange={(newTheme) => {
|
||||
setTheme(newTheme.config);
|
||||
}}
|
||||
locale={lang === 'cn' ? zhCN : enUS}
|
||||
actions={
|
||||
<Button type="primary" onClick={handleSave}>
|
||||
{locale.save}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { UnifiedTransformer } from 'dumi';
|
||||
import { unistUtilVisit } from 'dumi';
|
||||
import set from 'lodash/set';
|
||||
import semver from 'semver';
|
||||
|
||||
let hastToString: typeof import('hast-util-to-string').toString;
|
||||
|
||||
@@ -9,6 +10,24 @@ let hastToString: typeof import('hast-util-to-string').toString;
|
||||
({ toString: hastToString } = await import('hast-util-to-string'));
|
||||
})();
|
||||
|
||||
function isValidStrictVer(ver: string): boolean {
|
||||
if (!semver.valid(ver)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = ver.split('.');
|
||||
if (parts.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parts.every((part) => /^\d+$/.test(part));
|
||||
}
|
||||
|
||||
function isValidDate(dateStr: string): boolean {
|
||||
// (YYYY-MM-DD)
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(dateStr);
|
||||
}
|
||||
|
||||
const COMPONENT_NAME = 'RefinedChangelog';
|
||||
|
||||
function rehypeChangelog(): UnifiedTransformer<any> {
|
||||
@@ -23,32 +42,73 @@ function rehypeChangelog(): UnifiedTransformer<any> {
|
||||
const nodesToWrap: { parent: any; startIdx: number }[] = [];
|
||||
const WRAPPER_FLAG = 'data-changelog-wrapped'; // 包裹容器唯一标识
|
||||
|
||||
function checkLogSegment(node: any, strict = true) {
|
||||
if (node && node.type === 'element' && node.tagName === 'h2') {
|
||||
if (strict) {
|
||||
const ver = hastToString(node);
|
||||
return isValidStrictVer(ver) && semver.major(ver) >= 5;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unistUtilVisit.visit(tree, 'element', (node, idx, parent) => {
|
||||
if (node.properties?.[WRAPPER_FLAG]) {
|
||||
return unistUtilVisit.SKIP;
|
||||
}
|
||||
if (
|
||||
idx !== undefined &&
|
||||
parent &&
|
||||
idx! + 2 < parent.children.length &&
|
||||
node.tagName === 'h2' &&
|
||||
parent.children[idx! + 1].tagName === 'p' &&
|
||||
parent.children[idx! + 2].tagName === 'ul'
|
||||
) {
|
||||
|
||||
if (idx !== undefined && parent && checkLogSegment(node)) {
|
||||
nodesToWrap.push({ parent, startIdx: idx! });
|
||||
}
|
||||
});
|
||||
|
||||
nodesToWrap.reverse().forEach(({ parent, startIdx }) => {
|
||||
const [heading, date, list] = parent.children.splice(startIdx, 3);
|
||||
const totalNodesToWrap = nodesToWrap.length;
|
||||
for (let i = totalNodesToWrap - 1; i >= 0; i--) {
|
||||
const { parent, startIdx } = nodesToWrap[i];
|
||||
|
||||
let endIdx = -1;
|
||||
const isEndOfWrap = i === totalNodesToWrap - 1;
|
||||
for (let j = startIdx + 1; j < parent.children.length; j++) {
|
||||
const nextNode = parent.children[j];
|
||||
if (
|
||||
(isEndOfWrap && checkLogSegment(nextNode, false)) || // 日志页通常还存在历史 major 版本
|
||||
nextNode.properties?.[WRAPPER_FLAG] || // 已经被处理
|
||||
checkLogSegment(nextNode) // 下一段日志
|
||||
) {
|
||||
endIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (endIdx === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Version
|
||||
const heading = parent.children[startIdx];
|
||||
|
||||
// Find Date
|
||||
let dateIdx = -1;
|
||||
for (let j = startIdx + 1; j < endIdx; j++) {
|
||||
const node = parent.children[j];
|
||||
if (node.type === 'element' && isValidDate(hastToString(node))) {
|
||||
dateIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dateIdx === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect list nodes between dateIdx and endIdx
|
||||
const version = hastToString(heading);
|
||||
const date = parent.children[dateIdx];
|
||||
const dateStr = hastToString(date);
|
||||
const details = parent.children.slice(dateIdx + 1, endIdx);
|
||||
|
||||
const headingWrap = {
|
||||
type: 'element',
|
||||
tagName: `${COMPONENT_NAME}.Version`,
|
||||
// 为标签添加语义化 className (下面同理)
|
||||
children: [set(heading, 'properties.className', 'changelog-version')],
|
||||
};
|
||||
|
||||
@@ -58,10 +118,13 @@ function rehypeChangelog(): UnifiedTransformer<any> {
|
||||
children: [set(date, 'properties.className', 'changelog-date')],
|
||||
};
|
||||
|
||||
const listWrap = {
|
||||
const detailWrap = {
|
||||
type: 'element',
|
||||
tagName: `${COMPONENT_NAME}.Details`,
|
||||
children: [set(list, 'properties.className', 'changelog-details')],
|
||||
properties: {
|
||||
className: 'changelog-details',
|
||||
},
|
||||
children: details,
|
||||
};
|
||||
|
||||
const wrapper = {
|
||||
@@ -82,11 +145,11 @@ function rehypeChangelog(): UnifiedTransformer<any> {
|
||||
value: JSON.stringify(dateStr),
|
||||
},
|
||||
],
|
||||
children: [headingWrap, dateWrap, listWrap],
|
||||
children: [headingWrap, dateWrap, detailWrap],
|
||||
};
|
||||
|
||||
parent.children.splice(startIdx, 0, wrapper);
|
||||
});
|
||||
parent.children.splice(startIdx, endIdx - startIdx, wrapper);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ const remarkAnchor = (opt: Options = {}): UnifiedTransformer<any> => {
|
||||
const ids = new Set();
|
||||
|
||||
unistUtilVisit.visit(tree, 'heading', (node) => {
|
||||
if (toArr(realOpt.level).indexOf(node.depth) === -1) {
|
||||
if (!toArr(realOpt.level).includes(node.depth)) {
|
||||
return unistUtilVisit.CONTINUE;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,10 @@ interface AntdProps {
|
||||
component: keyof typeof all;
|
||||
}
|
||||
|
||||
function Antd(props: AntdProps) {
|
||||
const Antd: React.FC<AntdProps> = (props) => {
|
||||
const { component, ...restProps } = props;
|
||||
const Component = (all[component] ?? React.Fragment) as React.ComponentType;
|
||||
|
||||
const Component = (all[component] ?? React.Fragment) as React.ComponentType<any>;
|
||||
return <Component {...restProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
export default Antd;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Tag, TagProps } from 'antd';
|
||||
import { Tag } from 'antd';
|
||||
import type { TagProps } from 'antd';
|
||||
|
||||
// https://github.com/umijs/dumi/blob/master/src/client/theme-default/builtins/Badge/index.tsx
|
||||
interface BadgeProps extends TagProps {
|
||||
|
||||
@@ -1,46 +1,66 @@
|
||||
import React from 'react';
|
||||
import { EditOutlined, GithubOutlined, HistoryOutlined, CompassOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
BugOutlined,
|
||||
CompassOutlined,
|
||||
EditOutlined,
|
||||
GithubOutlined,
|
||||
HistoryOutlined,
|
||||
IssuesCloseOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { GetProp } from 'antd';
|
||||
import { Descriptions, Flex, theme, Tooltip, Typography } from 'antd';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import Link from '../../common/Link';
|
||||
|
||||
import useIssueCount from '../../../hooks/useIssueCount';
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import ComponentChangelog from '../../common/ComponentChangelog';
|
||||
import Link from '../../common/Link';
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
import: '使用',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
source: '源码',
|
||||
source: '反馈',
|
||||
docs: '文档',
|
||||
edit: '编辑此页',
|
||||
changelog: '更新日志',
|
||||
design: '设计指南',
|
||||
version: '版本',
|
||||
issueNew: '提交问题',
|
||||
issueOpen: '待解决',
|
||||
},
|
||||
en: {
|
||||
import: 'Import',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
source: 'Source',
|
||||
source: 'GitHub',
|
||||
docs: 'Docs',
|
||||
edit: 'Edit this page',
|
||||
changelog: 'Changelog',
|
||||
design: 'Design',
|
||||
version: 'Version',
|
||||
issueNew: 'Issue',
|
||||
issueOpen: 'Open issues',
|
||||
},
|
||||
};
|
||||
|
||||
const branchUrl = 'https://github.com/ant-design/ant-design/edit/master/';
|
||||
const branchUrl = (repo: string) => `https://github.com/${repo}/edit/master/`;
|
||||
|
||||
function isVersionNumber(value?: string) {
|
||||
return value && /^\d+\.\d+\.\d+$/.test(value);
|
||||
}
|
||||
|
||||
const transformComponentName = (componentName: string) => {
|
||||
if (componentName === 'Notification' || componentName === 'Message') {
|
||||
return componentName.toLowerCase();
|
||||
}
|
||||
return componentName;
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ token }) => ({
|
||||
code: css`
|
||||
cursor: pointer;
|
||||
@@ -61,7 +81,7 @@ const useStyle = createStyles(({ token }) => ({
|
||||
}
|
||||
`,
|
||||
icon: css`
|
||||
margin-inline-end: 3px;
|
||||
margin-inline-end: 4px;
|
||||
`,
|
||||
}));
|
||||
|
||||
@@ -71,15 +91,23 @@ export interface ComponentMetaProps {
|
||||
filename?: string;
|
||||
version?: string;
|
||||
designUrl?: string;
|
||||
searchTitleKeywords?: string[];
|
||||
repo: string;
|
||||
}
|
||||
|
||||
const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
|
||||
const { component, source, filename, version, designUrl } = props;
|
||||
const { component, source, filename, version, designUrl, searchTitleKeywords, repo } = props;
|
||||
const { token } = theme.useToken();
|
||||
const [locale, lang] = useLocale(locales);
|
||||
const isZhCN = lang === 'cn';
|
||||
const { styles } = useStyle();
|
||||
|
||||
// ======================= Issues Count =======================
|
||||
const { issueCount, issueCountLoading, issueNewUrl, issueSearchUrl } = useIssueCount({
|
||||
repo,
|
||||
titleKeywords: searchTitleKeywords,
|
||||
});
|
||||
|
||||
// ========================= Copy =========================
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
@@ -98,7 +126,7 @@ const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
|
||||
if (String(source) === 'true') {
|
||||
const kebabComponent = kebabCase(component);
|
||||
return [
|
||||
`https://github.com/ant-design/ant-design/blob/master/components/${kebabComponent}`,
|
||||
`https://github.com/${repo}/blob/master/components/${kebabComponent}`,
|
||||
`components/${kebabComponent}`,
|
||||
];
|
||||
}
|
||||
@@ -108,14 +136,7 @@ const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
|
||||
}
|
||||
|
||||
return [source, source];
|
||||
}, [component, source]);
|
||||
|
||||
const transformComponentName = (componentName: string) => {
|
||||
if (componentName === 'Notification' || componentName === 'Message') {
|
||||
return componentName.toLowerCase();
|
||||
}
|
||||
return componentName;
|
||||
};
|
||||
}, [component, repo, source]);
|
||||
|
||||
// ======================== Render ========================
|
||||
const importList = `import { ${transformComponentName(component)} } from "antd";`;
|
||||
@@ -126,9 +147,7 @@ const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
|
||||
colon={false}
|
||||
column={1}
|
||||
style={{ marginTop: token.margin }}
|
||||
styles={{
|
||||
label: { paddingInlineEnd: token.padding, width: 56 },
|
||||
}}
|
||||
styles={{ label: { paddingInlineEnd: token.padding, width: 56 } }}
|
||||
items={
|
||||
[
|
||||
{
|
||||
@@ -150,10 +169,22 @@ const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
|
||||
filledSource && {
|
||||
label: locale.source,
|
||||
children: (
|
||||
<Typography.Link className={styles.code} href={filledSource} target="_blank">
|
||||
<GithubOutlined className={styles.icon} />
|
||||
<span>{abbrSource}</span>
|
||||
</Typography.Link>
|
||||
<Flex justify="flex-start" align="center" gap="small">
|
||||
<Typography.Link className={styles.code} href={filledSource} target="_blank">
|
||||
<GithubOutlined className={styles.icon} />
|
||||
<span>{abbrSource}</span>
|
||||
</Typography.Link>
|
||||
<Typography.Link className={styles.code} href={issueNewUrl} target="_blank">
|
||||
<BugOutlined className={styles.icon} />
|
||||
<span>{locale.issueNew}</span>
|
||||
</Typography.Link>
|
||||
<Typography.Link className={styles.code} href={issueSearchUrl} target="_blank">
|
||||
<IssuesCloseOutlined className={styles.icon} />
|
||||
<span>
|
||||
{locale.issueOpen} {issueCountLoading ? <LoadingOutlined /> : issueCount}
|
||||
</span>
|
||||
</Typography.Link>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
filename && {
|
||||
@@ -162,7 +193,7 @@ const ComponentMeta: React.FC<ComponentMetaProps> = (props) => {
|
||||
<Flex justify="flex-start" align="center" gap="small">
|
||||
<Typography.Link
|
||||
className={styles.code}
|
||||
href={`${branchUrl}${filename}`}
|
||||
href={`${branchUrl(repo)}${filename}`}
|
||||
target="_blank"
|
||||
>
|
||||
<EditOutlined className={styles.icon} />
|
||||
|
||||
@@ -223,6 +223,7 @@ const Overview: React.FC = () => {
|
||||
>
|
||||
<div className={styles.componentsOverviewImg}>
|
||||
<img
|
||||
draggable={false}
|
||||
src={
|
||||
theme.includes('dark') && component.coverDark
|
||||
? component.coverDark
|
||||
|
||||
@@ -62,7 +62,7 @@ const DemoWrapper: typeof DumiDemoGrid = ({ items }) => {
|
||||
},
|
||||
});
|
||||
}, []),
|
||||
[expandAll, showDebug],
|
||||
[expandAll, items, showDebug],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
62
.dumi/theme/builtins/FlexWithImagePreview/index.tsx
Normal file
62
.dumi/theme/builtins/FlexWithImagePreview/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { Flex } from 'antd';
|
||||
import type { FlexProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImagePreview from '../ImagePreview';
|
||||
import type { ImagePreviewProps } from '../ImagePreview';
|
||||
|
||||
const isNotEmpty = (val: any) => {
|
||||
return typeof val !== 'undefined' && val !== null && val !== '';
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ css, token }) => {
|
||||
return {
|
||||
wrapper: css`
|
||||
color: ${token.colorText};
|
||||
font-size: ${token.fontSize}px;
|
||||
line-height: 2;
|
||||
`,
|
||||
title: css`
|
||||
margin: 1em 0;
|
||||
`,
|
||||
description: css`
|
||||
margin: 1em 0;
|
||||
padding-inline-start: 0.8em;
|
||||
color: ${token.colorTextSecondary};
|
||||
font-size: 90%;
|
||||
border-inline-start: 4px solid ${token.colorSplit};
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface FlexWithImagePreviewProps {
|
||||
imagePreviewProps?: ImagePreviewProps;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const FlexWithImagePreview: React.FC<
|
||||
FlexWithImagePreviewProps & React.PropsWithChildren<FlexProps>
|
||||
> = (props) => {
|
||||
const { imagePreviewProps, title, description, className, style, children, ...rest } = props;
|
||||
const { styles } = useStyle();
|
||||
if (!title && !description) {
|
||||
return <ImagePreview {...imagePreviewProps}>{children}</ImagePreview>;
|
||||
}
|
||||
return (
|
||||
<Flex className={classNames(styles.wrapper, className)} style={style} {...rest}>
|
||||
<Flex align="flex-start" justify="flex-start" vertical>
|
||||
{isNotEmpty(title) && <div className={styles.title}>{title}</div>}
|
||||
{isNotEmpty(description) && <div className={styles.description}>{description}</div>}
|
||||
</Flex>
|
||||
<ImagePreview {...imagePreviewProps}>{children}</ImagePreview>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlexWithImagePreview;
|
||||
@@ -30,7 +30,7 @@ interface CategoryProps {
|
||||
title: CategoriesKeys;
|
||||
icons: string[];
|
||||
theme: ThemeType;
|
||||
newIcons: string[];
|
||||
newIcons: ReadonlyArray<string> | string[];
|
||||
}
|
||||
|
||||
const Category: React.FC<CategoryProps> = (props) => {
|
||||
@@ -40,17 +40,20 @@ const Category: React.FC<CategoryProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const [justCopied, setJustCopied] = React.useState<string | null>(null);
|
||||
const copyId = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const onCopied = React.useCallback((type: string, text: string) => {
|
||||
message.success(
|
||||
<span>
|
||||
<code className={styles.copiedCode}>{text}</code> copied 🎉
|
||||
</span>,
|
||||
);
|
||||
setJustCopied(type);
|
||||
copyId.current = setTimeout(() => {
|
||||
setJustCopied(null);
|
||||
}, 2000);
|
||||
}, []);
|
||||
const onCopied = React.useCallback(
|
||||
(type: string, text: string) => {
|
||||
message.success(
|
||||
<span>
|
||||
<code className={styles.copiedCode}>{text}</code> copied 🎉
|
||||
</span>,
|
||||
);
|
||||
setJustCopied(type);
|
||||
copyId.current = setTimeout(() => {
|
||||
setJustCopied(null);
|
||||
}, 2000);
|
||||
},
|
||||
[message, styles.copiedCode],
|
||||
);
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
if (copyId.current) {
|
||||
|
||||
@@ -9,7 +9,9 @@ import debounce from 'lodash/debounce';
|
||||
|
||||
import Category from './Category';
|
||||
import type { CategoriesKeys } from './fields';
|
||||
import { categories } from './fields';
|
||||
import { all, categories } from './fields';
|
||||
import metaInfo from './meta';
|
||||
import type { IconName, IconsMeta } from './meta';
|
||||
import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons';
|
||||
|
||||
export enum ThemeType {
|
||||
@@ -33,6 +35,8 @@ interface IconSearchState {
|
||||
searchKey: string;
|
||||
}
|
||||
|
||||
const NEW_ICON_NAMES: ReadonlyArray<string> = [];
|
||||
|
||||
const IconSearch: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { styles } = useStyle();
|
||||
@@ -42,8 +46,6 @@ const IconSearch: React.FC = () => {
|
||||
});
|
||||
const token = useTheme();
|
||||
|
||||
const newIconNames: string[] = [];
|
||||
|
||||
const handleSearchIcon = debounce((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDisplayState((prevState) => ({ ...prevState, searchKey: e.target.value }));
|
||||
}, 300);
|
||||
@@ -54,16 +56,23 @@ const IconSearch: React.FC = () => {
|
||||
|
||||
const renderCategories = useMemo<React.ReactNode | React.ReactNode[]>(() => {
|
||||
const { searchKey = '', theme } = displayState;
|
||||
// loop over metaInfo to find all the icons which has searchKey in their tags
|
||||
let normalizedSearchKey = searchKey?.trim();
|
||||
|
||||
const categoriesResult = Object.keys(categories)
|
||||
.map((key) => {
|
||||
if (normalizedSearchKey) {
|
||||
normalizedSearchKey = normalizedSearchKey
|
||||
.replace(/^<([a-z]*)\s\/>$/gi, (_, name) => name)
|
||||
.replace(/(Filled|Outlined|TwoTone)$/, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
const tagMatchedCategoryObj = matchCategoriesFromTag(normalizedSearchKey, metaInfo);
|
||||
|
||||
const namedMatchedCategoryObj = Object.keys(categories).reduce<Record<string, MatchedCategory>>(
|
||||
(acc, key) => {
|
||||
let iconList = categories[key as CategoriesKeys];
|
||||
if (searchKey) {
|
||||
const matchKey = searchKey
|
||||
|
||||
.replace(/^<([a-z]*)\s\/>$/gi, (_, name) => name)
|
||||
.replace(/(Filled|Outlined|TwoTone)$/, '')
|
||||
.toLowerCase();
|
||||
if (normalizedSearchKey) {
|
||||
const matchKey = normalizedSearchKey;
|
||||
iconList = iconList.filter((iconName) => iconName.toLowerCase().includes(matchKey));
|
||||
}
|
||||
|
||||
@@ -73,27 +82,42 @@ const IconSearch: React.FC = () => {
|
||||
];
|
||||
iconList = iconList.filter((icon) => !ignore.includes(icon));
|
||||
|
||||
return {
|
||||
acc[key] = {
|
||||
category: key,
|
||||
icons: iconList
|
||||
.map((iconName) => iconName + theme)
|
||||
.filter((iconName) => allIcons[iconName]),
|
||||
icons: iconList,
|
||||
};
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
// merge matched categories from tag search
|
||||
const merged = mergeCategory(namedMatchedCategoryObj, tagMatchedCategoryObj);
|
||||
const matchedCategories = Object.values(merged)
|
||||
.map((item) => {
|
||||
item.icons = item.icons
|
||||
.map((iconName) => iconName + theme)
|
||||
.filter((iconName) => allIcons[iconName]);
|
||||
|
||||
return item;
|
||||
})
|
||||
.filter(({ icons }) => !!icons.length)
|
||||
.map(({ category, icons }) => (
|
||||
<Category
|
||||
key={category}
|
||||
title={category as CategoriesKeys}
|
||||
theme={theme}
|
||||
icons={icons}
|
||||
newIcons={newIconNames}
|
||||
/>
|
||||
));
|
||||
.filter(({ icons }) => !!icons.length);
|
||||
|
||||
const categoriesResult = matchedCategories.map(({ category, icons }) => (
|
||||
<Category
|
||||
key={category}
|
||||
title={category as CategoriesKeys}
|
||||
theme={theme}
|
||||
icons={icons}
|
||||
newIcons={NEW_ICON_NAMES}
|
||||
/>
|
||||
));
|
||||
return categoriesResult.length ? categoriesResult : <Empty style={{ margin: '2em 0' }} />;
|
||||
}, [displayState.searchKey, displayState.theme]);
|
||||
}, [displayState]);
|
||||
|
||||
const [searchBarAffixed, setSearchBarAffixed] = useState<boolean | undefined>(false);
|
||||
|
||||
const { borderRadius, colorBgContainer, anchorTop } = token;
|
||||
|
||||
const affixedStyle: CSSProperties = {
|
||||
@@ -136,7 +160,10 @@ const IconSearch: React.FC = () => {
|
||||
onChange={handleChangeTheme}
|
||||
/>
|
||||
<Input.Search
|
||||
placeholder={intl.formatMessage({ id: 'app.docs.components.icon.search.placeholder' })}
|
||||
placeholder={intl.formatMessage(
|
||||
{ id: 'app.docs.components.icon.search.placeholder' },
|
||||
{ total: all.length },
|
||||
)}
|
||||
style={{ flex: 1, marginInlineStart: 16 }}
|
||||
allowClear
|
||||
autoFocus
|
||||
@@ -151,3 +178,49 @@ const IconSearch: React.FC = () => {
|
||||
};
|
||||
|
||||
export default IconSearch;
|
||||
|
||||
type MatchedCategory = {
|
||||
category: string;
|
||||
icons: string[];
|
||||
};
|
||||
|
||||
function matchCategoriesFromTag(searchKey: string, metaInfo: IconsMeta) {
|
||||
if (!searchKey) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.keys(metaInfo).reduce<Record<string, MatchedCategory>>((acc, key) => {
|
||||
const icon = metaInfo[key as IconName];
|
||||
const category = icon.category;
|
||||
|
||||
if (icon.tags.some((tag) => tag.toLowerCase().includes(searchKey))) {
|
||||
if (acc[category]) {
|
||||
// if category exists, push icon to icons array
|
||||
acc[category].icons.push(key);
|
||||
} else {
|
||||
// if category does not exist, create a new entry
|
||||
acc[category] = { category, icons: [key] };
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function mergeCategory(
|
||||
categoryA: Record<string, MatchedCategory>,
|
||||
categoryB: Record<string, MatchedCategory>,
|
||||
) {
|
||||
const merged: Record<string, MatchedCategory> = { ...categoryA };
|
||||
|
||||
Object.keys(categoryB).forEach((key) => {
|
||||
if (merged[key]) {
|
||||
// merge icons array and remove duplicates
|
||||
merged[key].icons = Array.from(new Set([...merged[key].icons, ...categoryB[key].icons]));
|
||||
} else {
|
||||
merged[key] = categoryB[key];
|
||||
}
|
||||
});
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as AntdIcons from '@ant-design/icons/lib/icons';
|
||||
|
||||
const all = Object.keys(AntdIcons)
|
||||
export const all = Object.keys(AntdIcons)
|
||||
.map((n) => n.replace(/(Outlined|Filled|TwoTone)$/, ''))
|
||||
.filter((n, i, arr) => arr.indexOf(n) === i);
|
||||
|
||||
|
||||
8
.dumi/theme/builtins/IconSearch/meta/AccountBook.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/AccountBook.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial, 'ledger'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Alipay.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Alipay.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial],
|
||||
category: 'logo',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Bank.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Bank.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/CarryOut.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/CarryOut.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: check,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Check.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Check.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: check,
|
||||
category: 'suggestion',
|
||||
} as const satisfies IconMetaSchema;
|
||||
7
.dumi/theme/builtins/IconSearch/meta/ClockSquare.ts
Normal file
7
.dumi/theme/builtins/IconSearch/meta/ClockSquare.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: ['time', 'watch', 'alarm'],
|
||||
category: 'suggestion',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Comment.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Comment.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis, 'feedback', 'discussion', 'reply', 'opinion', 'note'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
7
.dumi/theme/builtins/IconSearch/meta/Copy.ts
Normal file
7
.dumi/theme/builtins/IconSearch/meta/Copy.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: ['复制', 'clone', 'duplicate'],
|
||||
category: 'editor',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/CreditCard.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/CreditCard.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Dash.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Dash.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis],
|
||||
category: 'editor',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Dollar.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Dollar.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Ellipsis.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Ellipsis.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Euro.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Euro.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Holder.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Holder.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/IssuesClose.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/IssuesClose.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...check],
|
||||
category: 'suggestion',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/More.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/More.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ellipsis } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...ellipsis, 'vertical'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/PayCircle.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/PayCircle.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/PoundCircle.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/PoundCircle.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/PropertySafety.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/PropertySafety.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial, 'safety', 'protection', 'security', 'shield'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/RedEnvelope.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/RedEnvelope.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: financial,
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
7
.dumi/theme/builtins/IconSearch/meta/Robot.ts
Normal file
7
.dumi/theme/builtins/IconSearch/meta/Robot.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: ['ai'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Safety.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Safety.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...check, 'protection', 'security', 'shield', 'safety', 'privacy'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Schedule.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Schedule.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { check } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...check, 'time', 'clock', 'calendar'],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
8
.dumi/theme/builtins/IconSearch/meta/Transaction.ts
Normal file
8
.dumi/theme/builtins/IconSearch/meta/Transaction.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { financial } from './tags';
|
||||
import type { IconMetaSchema } from './tags';
|
||||
|
||||
export default {
|
||||
contributors: ['ant-design'],
|
||||
tags: [...financial],
|
||||
category: 'other',
|
||||
} as const satisfies IconMetaSchema;
|
||||
62
.dumi/theme/builtins/IconSearch/meta/index.ts
Normal file
62
.dumi/theme/builtins/IconSearch/meta/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import AccountBook from './AccountBook';
|
||||
import Alipay from './Alipay';
|
||||
import Bank from './Bank';
|
||||
import CarryOut from './CarryOut';
|
||||
import Check from './Check';
|
||||
import ClockSquare from './ClockSquare';
|
||||
import Comment from './Comment';
|
||||
import Copy from './Copy';
|
||||
import CreditCard from './CreditCard';
|
||||
import Dash from './Dash';
|
||||
import Dollar from './Dollar';
|
||||
import Ellipsis from './Ellipsis';
|
||||
import Euro from './Euro';
|
||||
import Holder from './Holder';
|
||||
import IssuesClose from './IssuesClose';
|
||||
import More from './More';
|
||||
import PayCircle from './PayCircle';
|
||||
import PoundCircle from './PoundCircle';
|
||||
import PropertySafety from './PropertySafety';
|
||||
import RedEnvelope from './RedEnvelope';
|
||||
import Robot from './Robot';
|
||||
import Safety from './Safety';
|
||||
import Schedule from './Schedule';
|
||||
import Transaction from './Transaction';
|
||||
|
||||
const all = {
|
||||
AccountBook,
|
||||
Alipay,
|
||||
AlipayCircle: Alipay,
|
||||
Bank,
|
||||
CarryOut,
|
||||
Check,
|
||||
CheckCircle: Check,
|
||||
CheckSquare: Check,
|
||||
ClockSquare,
|
||||
Comment,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Dash,
|
||||
SmallDash: Dash,
|
||||
Dollar,
|
||||
Ellipsis,
|
||||
Euro,
|
||||
EuroCircle: Euro,
|
||||
Holder,
|
||||
IssuesClose,
|
||||
More,
|
||||
PayCircle,
|
||||
PoundCircle,
|
||||
Pound: PoundCircle,
|
||||
PropertySafety,
|
||||
RedEnvelope,
|
||||
Robot,
|
||||
Safety,
|
||||
Schedule,
|
||||
Transaction,
|
||||
};
|
||||
|
||||
export type IconsMeta = typeof all;
|
||||
export type IconName = keyof IconsMeta;
|
||||
|
||||
export default all;
|
||||
87
.dumi/theme/builtins/IconSearch/meta/tags.ts
Normal file
87
.dumi/theme/builtins/IconSearch/meta/tags.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { CategoriesKeys } from '../fields';
|
||||
|
||||
export type IconMetaSchema = Readonly<{
|
||||
contributors: string[];
|
||||
tags: readonly string[];
|
||||
category: CategoriesKeys;
|
||||
}>;
|
||||
|
||||
export const check = [
|
||||
'check',
|
||||
'done',
|
||||
'todo',
|
||||
'tick',
|
||||
'complete',
|
||||
'finish',
|
||||
'task',
|
||||
|
||||
'ok',
|
||||
'success',
|
||||
'confirm',
|
||||
'approve',
|
||||
'agree',
|
||||
'validation',
|
||||
|
||||
'√',
|
||||
'✔',
|
||||
'✓',
|
||||
'勾',
|
||||
'对',
|
||||
'正确',
|
||||
'right',
|
||||
] as const;
|
||||
|
||||
export const financial = [
|
||||
'monetization',
|
||||
'marketing',
|
||||
'currency',
|
||||
'money',
|
||||
'payment',
|
||||
'finance',
|
||||
'cash',
|
||||
'bank',
|
||||
'transaction',
|
||||
'balance',
|
||||
'expense',
|
||||
'income',
|
||||
'budget',
|
||||
'investment',
|
||||
'savings',
|
||||
'profit',
|
||||
'cost',
|
||||
'wealth',
|
||||
'economy',
|
||||
'wallet',
|
||||
'exchange',
|
||||
] as const;
|
||||
|
||||
export const ellipsis = [
|
||||
'...',
|
||||
'。。。',
|
||||
'…',
|
||||
'more',
|
||||
'更多',
|
||||
'dots',
|
||||
'ellipsis',
|
||||
'expand',
|
||||
'collapse',
|
||||
'menu',
|
||||
'dropdown',
|
||||
'options',
|
||||
'settings',
|
||||
'et cetera',
|
||||
'etc',
|
||||
'loader',
|
||||
'loading',
|
||||
'progress',
|
||||
'pending',
|
||||
'throbber',
|
||||
'spinner',
|
||||
'operator',
|
||||
'code',
|
||||
'spread',
|
||||
'rest',
|
||||
'further',
|
||||
'extra',
|
||||
'overflow',
|
||||
] as const;
|
||||
@@ -3,7 +3,7 @@ import { Image } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import toArray from 'rc-util/lib/Children/toArray';
|
||||
|
||||
interface ImagePreviewProps {
|
||||
export interface ImagePreviewProps {
|
||||
className?: string;
|
||||
/** Do not show padding & background */
|
||||
pure?: boolean;
|
||||
@@ -65,7 +65,7 @@ const ImagePreview: React.FC<React.PropsWithChildren<ImagePreviewProps>> = (prop
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="image-modal-container">
|
||||
<img {...metaCopy} src={meta.src} alt={meta.alt} />
|
||||
<img {...metaCopy} draggable={false} src={meta.src} alt={meta.alt} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ const LocaleLink: React.FC<React.PropsWithChildren<LocaleLinkProps>> = ({
|
||||
}
|
||||
|
||||
return to;
|
||||
}, [to]);
|
||||
}, [localeType, to]);
|
||||
|
||||
const linkProps: LocaleLinkProps = {
|
||||
...props,
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import React, { useRef, Suspense } from 'react';
|
||||
import React, { Suspense, useRef } from 'react';
|
||||
import { LinkOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import stackblitzSdk from '@stackblitz/sdk';
|
||||
import type { Project } from '@stackblitz/sdk';
|
||||
import { Flex, Tooltip } from 'antd';
|
||||
import { FormattedMessage, useSiteData } from 'dumi';
|
||||
import LZString from 'lz-string';
|
||||
import stackblitzSdk from '@stackblitz/sdk';
|
||||
|
||||
import type { Project } from '@stackblitz/sdk';
|
||||
|
||||
import DemoContext from '../../slots/DemoContext';
|
||||
import packageJson from '../../../../package.json';
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import ClientOnly from '../../common/ClientOnly';
|
||||
import CodePenIcon from '../../icons/CodePenIcon';
|
||||
import CodeSandboxIcon from '../../icons/CodeSandboxIcon';
|
||||
import ExternalLinkIcon from '../../icons/ExternalLinkIcon';
|
||||
import ExpandIcon from '../../icons/ExpandIcon';
|
||||
import ClientOnly from '../../common/ClientOnly';
|
||||
import ExternalLinkIcon from '../../icons/ExternalLinkIcon';
|
||||
import DemoContext from '../../slots/DemoContext';
|
||||
import CodeBlockButton from './CodeBlockButton';
|
||||
import getStackblitzConfig from './stackblitzConfig';
|
||||
|
||||
const track = ({ type, demo }: { type: string; demo: string }) => {
|
||||
window.gtag?.('event', 'demo', { event_category: type, event_label: demo });
|
||||
@@ -54,6 +55,8 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
entryCode,
|
||||
styleCode,
|
||||
}) => {
|
||||
const [, lang] = useLocale();
|
||||
const isZhCN = lang === 'cn';
|
||||
const { pkg } = useSiteData();
|
||||
const { codeType } = React.use(DemoContext);
|
||||
const codeSandboxIconRef = useRef<HTMLFormElement>(null);
|
||||
@@ -78,18 +81,6 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
</html>
|
||||
`;
|
||||
|
||||
const tsconfig = {
|
||||
compilerOptions: {
|
||||
target: 'esnext',
|
||||
module: 'esnext',
|
||||
esModuleInterop: true,
|
||||
moduleResolution: 'node',
|
||||
jsx: 'react',
|
||||
jsxFactory: 'React.createElement',
|
||||
jsxFragmentFactory: 'React.Fragment',
|
||||
},
|
||||
};
|
||||
|
||||
const suffix = codeType === 'tsx' ? 'tsx' : 'js';
|
||||
|
||||
const dependencies = (jsx as string).split('\n').reduce<Record<PropertyKey, string>>(
|
||||
@@ -161,7 +152,7 @@ const Actions: React.FC<ActionsProps> = ({
|
||||
}
|
||||
const demoJsContent = `
|
||||
${importReactContent}
|
||||
import './index.css';
|
||||
${styleCode ? `import './index.css';` : ''}
|
||||
${parsedSourceCode}
|
||||
`.trim();
|
||||
const indexCssContent = (styleCode || '')
|
||||
@@ -213,9 +204,8 @@ createRoot(document.getElementById('container')).render(<Demo />);
|
||||
},
|
||||
};
|
||||
|
||||
const stackblitzPrefillConfig: Project = {
|
||||
const stackblitzPrefillConfig: Project = getStackblitzConfig({
|
||||
title: `${title} - antd@${dependencies.antd}`,
|
||||
template: 'create-react-app',
|
||||
dependencies: {
|
||||
...dependencies,
|
||||
react: '^19.0.0',
|
||||
@@ -224,18 +214,11 @@ createRoot(document.getElementById('container')).render(<Demo />);
|
||||
'@types/react-dom': '^19.0.0',
|
||||
'@ant-design/v5-patch-for-react-19': '^1.0.3',
|
||||
},
|
||||
description: '',
|
||||
files: {
|
||||
'index.css': indexCssContent,
|
||||
[`index.${suffix}`]: `import '@ant-design/v5-patch-for-react-19';\n${indexJsContent}`,
|
||||
[`demo.${suffix}`]: demoJsContent,
|
||||
'index.html': html,
|
||||
},
|
||||
};
|
||||
|
||||
if (suffix === 'tsx') {
|
||||
stackblitzPrefillConfig.files['tsconfig.json'] = JSON.stringify(tsconfig, null, 2);
|
||||
}
|
||||
demoJsContent,
|
||||
indexCssContent,
|
||||
suffix,
|
||||
isZhCN,
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex wrap gap="middle" className="code-box-actions">
|
||||
@@ -287,7 +270,7 @@ createRoot(document.getElementById('container')).render(<Demo />);
|
||||
onClick={() => {
|
||||
track({ type: 'stackblitz', demo: assetId });
|
||||
stackblitzSdk.openProject(stackblitzPrefillConfig, {
|
||||
openFile: [`demo.${suffix}`],
|
||||
openFile: [`src/demo.${suffix === 'tsx' ? 'tsx' : 'jsx'}`],
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Suspense, useState } from 'react';
|
||||
import React, { Suspense, useMemo, useState } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { App, Tooltip } from 'antd';
|
||||
import { FormattedMessage } from 'dumi';
|
||||
@@ -27,17 +27,19 @@ const CodeBlockButton: React.FC<CodeBlockButtonProps> = ({ title, dependencies =
|
||||
|
||||
const [locale] = useLocale(locales);
|
||||
|
||||
const codeBlockPrefillConfig = {
|
||||
title: `${title} - antd@${dependencies.antd}`,
|
||||
js: `${
|
||||
/import React(\D*)from 'react';/.test(jsx) ? '' : `import React from 'react';\n`
|
||||
}import { createRoot } from 'react-dom/client';\n${jsx.replace(
|
||||
/export default/,
|
||||
'const ComponentDemo =',
|
||||
)}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
|
||||
css: '',
|
||||
json: JSON.stringify({ name: 'antd-demo', dependencies }, null, 2),
|
||||
};
|
||||
const codeBlockPrefillConfig = useMemo(() => {
|
||||
return {
|
||||
title: `${title} - antd@${dependencies.antd}`,
|
||||
js: `${
|
||||
/import React(\D*)from 'react';/.test(jsx) ? '' : `import React from 'react';\n`
|
||||
}import { createRoot } from 'react-dom/client';\n${jsx.replace(
|
||||
/export default/,
|
||||
'const ComponentDemo =',
|
||||
)}\n\ncreateRoot(mountNode).render(<ComponentDemo />);\n`,
|
||||
css: '',
|
||||
json: JSON.stringify({ name: 'antd-demo', dependencies }, null, 2),
|
||||
};
|
||||
}, [dependencies, jsx, title]);
|
||||
|
||||
const openHituCodeBlockFn = React.useCallback(() => {
|
||||
setLoading(false);
|
||||
@@ -52,8 +54,7 @@ const CodeBlockButton: React.FC<CodeBlockButtonProps> = ({ title, dependencies =
|
||||
|
||||
const handleClick = () => {
|
||||
const scriptId = 'hitu-code-block-js';
|
||||
const existScript = document.getElementById(scriptId) as HTMLScriptElement | null;
|
||||
// @ts-ignore
|
||||
const existScript = document.getElementById(scriptId) as HTMLScriptElement;
|
||||
if (existScript?.dataset.loaded) {
|
||||
openHituCodeBlockFn();
|
||||
return;
|
||||
@@ -86,7 +87,7 @@ const CodeBlockButton: React.FC<CodeBlockButtonProps> = ({ title, dependencies =
|
||||
);
|
||||
};
|
||||
|
||||
const SuspenseCodeBlockButton: React.FC<React.ComponentProps<typeof CodeBlockButton>> = (props) => (
|
||||
const SuspenseCodeBlockButton: React.FC<CodeBlockButtonProps> = (props) => (
|
||||
<Suspense fallback={null}>
|
||||
<CodeBlockButton {...props} />
|
||||
</Suspense>
|
||||
|
||||
@@ -98,7 +98,7 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => {
|
||||
if (asset.id === hash.slice(1)) {
|
||||
anchorRef.current?.click();
|
||||
}
|
||||
}, []);
|
||||
}, [asset.id, hash]);
|
||||
|
||||
useEffect(() => {
|
||||
setCodeExpand(expand);
|
||||
@@ -142,7 +142,12 @@ const CodePreviewer: React.FC<AntdPreviewerProps> = (props) => {
|
||||
|
||||
const codeBox: React.ReactNode = (
|
||||
<section className={codeBoxClass} id={asset.id}>
|
||||
<section className="code-box-demo" style={codeBoxDemoStyle} ref={demoContainer}>
|
||||
<section
|
||||
className="code-box-demo notranslate"
|
||||
translate="no"
|
||||
style={codeBoxDemoStyle}
|
||||
ref={demoContainer}
|
||||
>
|
||||
{liveDemoNode || <React.StrictMode>{previewDemo.current}</React.StrictMode>}
|
||||
</section>
|
||||
{!simplify && (
|
||||
|
||||
209
.dumi/theme/builtins/Previewer/stackblitzConfig.ts
Normal file
209
.dumi/theme/builtins/Previewer/stackblitzConfig.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import type { Project, ProjectFiles } from '@stackblitz/sdk';
|
||||
|
||||
const getStackblitzConfig = ({
|
||||
title = '',
|
||||
dependencies,
|
||||
indexCssContent = '',
|
||||
demoJsContent = '',
|
||||
suffix = '',
|
||||
isZhCN = false,
|
||||
}: {
|
||||
title?: string;
|
||||
dependencies: Record<string, string>;
|
||||
indexCssContent?: string;
|
||||
demoJsContent?: string;
|
||||
suffix?: string;
|
||||
isZhCN?: boolean;
|
||||
}) => {
|
||||
const _suffix = suffix === 'tsx' ? suffix : 'jsx';
|
||||
const packageJSON = {
|
||||
name: 'vite-react-typescript-starter',
|
||||
private: true,
|
||||
version: '0.0.0',
|
||||
type: 'module',
|
||||
scripts: {
|
||||
dev: 'vite',
|
||||
build: 'tsc -b && vite build',
|
||||
lint: 'eslint .',
|
||||
preview: 'vite preview',
|
||||
},
|
||||
dependencies,
|
||||
devDependencies: {
|
||||
'@eslint/js': '^9.32.0',
|
||||
'@types/react': '^19.1.9',
|
||||
'@types/react-dom': '^19.1.7',
|
||||
'@vitejs/plugin-react': '^4.7.0',
|
||||
eslint: '^9.32.0',
|
||||
'eslint-plugin-react-hooks': '^7.0.0',
|
||||
'eslint-plugin-react-refresh': '^0.4.20',
|
||||
globals: '^16.3.0',
|
||||
typescript: '~5.8.3',
|
||||
'typescript-eslint': '^8.39.0',
|
||||
vite: '^7.0.6',
|
||||
},
|
||||
};
|
||||
|
||||
const tsconfigAppJSON = {
|
||||
compilerOptions: {
|
||||
tsBuildInfoFile: './node_modules/.tmp/tsconfig.app.tsbuildinfo',
|
||||
target: 'ES2022',
|
||||
useDefineForClassFields: true,
|
||||
lib: ['ES2022', 'DOM', 'DOM.Iterable'],
|
||||
module: 'ESNext',
|
||||
skipLibCheck: true,
|
||||
|
||||
/* Bundler mode */
|
||||
moduleResolution: 'bundler',
|
||||
allowImportingTsExtensions: true,
|
||||
verbatimModuleSyntax: true,
|
||||
moduleDetection: 'force',
|
||||
noEmit: true,
|
||||
jsx: 'react-jsx',
|
||||
|
||||
/* Linting */
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
erasableSyntaxOnly: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
noUncheckedSideEffectImports: true,
|
||||
},
|
||||
include: ['src'],
|
||||
};
|
||||
const tsconfigNodeJSON = {
|
||||
compilerOptions: {
|
||||
tsBuildInfoFile: './node_modules/.tmp/tsconfig.node.tsbuildinfo',
|
||||
target: 'ES2023',
|
||||
lib: ['ES2023'],
|
||||
module: 'ESNext',
|
||||
skipLibCheck: true,
|
||||
|
||||
/* Bundler mode */
|
||||
moduleResolution: 'bundler',
|
||||
allowImportingTsExtensions: true,
|
||||
verbatimModuleSyntax: true,
|
||||
moduleDetection: 'force',
|
||||
noEmit: true,
|
||||
|
||||
/* Linting */
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
erasableSyntaxOnly: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
noUncheckedSideEffectImports: true,
|
||||
},
|
||||
include: ['vite.config.ts'],
|
||||
};
|
||||
|
||||
const tsconfigJSON = {
|
||||
files: [],
|
||||
references: [{ path: './tsconfig.app.json' }, { path: './tsconfig.node.json' }],
|
||||
};
|
||||
|
||||
let files: ProjectFiles = {
|
||||
// demo.tsx
|
||||
[`src/demo.${_suffix}`]: demoJsContent,
|
||||
// package.json
|
||||
'package.json': JSON.stringify(packageJSON, null, 4),
|
||||
// index.html
|
||||
'index.html': `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="theme-color" content="#000000">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container" style="padding: 24px" />
|
||||
<script type="module" src="/src/main.${_suffix}"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
// main.tsx
|
||||
[`src/main.${_suffix}`]: `import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import '@ant-design/v5-patch-for-react-19';
|
||||
import Demo from './demo';
|
||||
|
||||
createRoot(document.getElementById('container')${suffix === 'tsx' ? '!' : ''}).render(
|
||||
<StrictMode>
|
||||
<Demo />
|
||||
</StrictMode>
|
||||
);`,
|
||||
// vite.config.ts
|
||||
'vite.config.ts': `import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})`,
|
||||
// .stackblitzrc
|
||||
'.stackblitzrc': `{
|
||||
"installDependencies": false,
|
||||
"startCommand": "pnpm i & pnpm dev",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
}`,
|
||||
// .gitignore
|
||||
'.gitignore': `# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local`,
|
||||
// eslint.config.js
|
||||
'eslint.config.js': `import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
`,
|
||||
};
|
||||
|
||||
if (suffix === 'tsx') {
|
||||
files = {
|
||||
...files,
|
||||
'tsconfig.json': JSON.stringify(tsconfigJSON, null, 4),
|
||||
'tsconfig.app.json': JSON.stringify(tsconfigAppJSON, null, 4),
|
||||
'tsconfig.node.json': JSON.stringify(tsconfigNodeJSON, null, 4),
|
||||
};
|
||||
}
|
||||
if (indexCssContent) {
|
||||
files = { ...files, 'src/index.css': indexCssContent };
|
||||
}
|
||||
if (isZhCN) {
|
||||
files = { ...files, '.npmrc': `registry=https://registry.npmmirror.com/` };
|
||||
}
|
||||
|
||||
const project: Project = { title, description: '', template: 'node', files };
|
||||
return project;
|
||||
};
|
||||
|
||||
export default getStackblitzConfig;
|
||||
@@ -2,7 +2,8 @@ import * as React from 'react';
|
||||
import { BugOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Popover, theme } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import { matchDeprecated } from '../../utils';
|
||||
@@ -118,7 +119,10 @@ const Version: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||
|
||||
const DateComp: React.FC<React.PropsWithChildren> = (props) => props.children;
|
||||
|
||||
const DetailsComp: React.FC<React.PropsWithChildren> = (props) => props.children;
|
||||
const DetailsComp: React.FC<React.PropsWithChildren<HTMLDivElement>> = (props) => {
|
||||
const { children, className } = props;
|
||||
return <div className={className}>{children}</div>;
|
||||
};
|
||||
|
||||
export default Object.assign(RefinedChangelog, {
|
||||
Version,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { Avatar, Divider, Empty, Skeleton, Tabs } from 'antd';
|
||||
import { Alert, Avatar, Divider, Empty, Skeleton, Tabs } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import dayjs from 'dayjs';
|
||||
import { FormattedMessage } from 'dumi';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import type { Article, Authors, SiteData } from '../../../pages/index/components/util';
|
||||
import { useSiteData } from '../../../pages/index/components/util';
|
||||
import { useAntdSiteConfig } from '../../../pages/index/components/util';
|
||||
|
||||
const useStyle = createStyles(({ token, css }) => {
|
||||
const { antCls } = token;
|
||||
@@ -92,7 +92,7 @@ const ArticleList: React.FC<ArticleListProps> = ({ name, data = [], authors = []
|
||||
);
|
||||
};
|
||||
|
||||
const Articles: React.FC<{ data: Partial<SiteData> }> = ({ data }) => {
|
||||
const Articles: React.FC<{ data?: Partial<SiteData> }> = ({ data = {} }) => {
|
||||
const [, lang] = useLocale();
|
||||
const isZhCN = lang === 'cn';
|
||||
|
||||
@@ -107,7 +107,7 @@ const Articles: React.FC<{ data: Partial<SiteData> }> = ({ data }) => {
|
||||
yearData[year][article.type] = [...(yearData[year][article.type] || []), article];
|
||||
});
|
||||
return yearData;
|
||||
}, [articles]);
|
||||
}, [articles, lang]);
|
||||
|
||||
const yearList = Object.keys(mergedData).sort((a, b) => Number(b) - Number(a));
|
||||
|
||||
@@ -145,15 +145,27 @@ const Articles: React.FC<{ data: Partial<SiteData> }> = ({ data }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const ResourceArticles: React.FC = () => {
|
||||
const { styles } = useStyle();
|
||||
const data = useSiteData();
|
||||
|
||||
const articles = data ? <Articles data={data} /> : <Skeleton active />;
|
||||
|
||||
const { data, error, isLoading } = useAntdSiteConfig();
|
||||
if (isLoading) {
|
||||
return <Skeleton active />;
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<Alert
|
||||
showIcon
|
||||
type="error"
|
||||
message={error.message}
|
||||
description={process.env.NODE_ENV !== 'production' ? error.stack : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div id="articles" className={styles.articles}>
|
||||
{articles}
|
||||
<Articles data={data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceArticles;
|
||||
|
||||
@@ -83,7 +83,11 @@ const ResourceCard: React.FC<ResourceCardProps> = ({ resource }) => {
|
||||
return (
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<a className={styles.card} target="_blank" href={src} rel="noreferrer">
|
||||
<Card hoverable className={styles.card} cover={<img src={cover} alt={title} />}>
|
||||
<Card
|
||||
hoverable
|
||||
className={styles.card}
|
||||
cover={<img draggable={false} src={cover} alt={title} />}
|
||||
>
|
||||
<Card.Meta
|
||||
title={title}
|
||||
description={
|
||||
|
||||
@@ -31,7 +31,7 @@ const useStyle = createStyles(({ token, css }) => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-inline-end: 1px solid rgba(0, 0, 0, 0.1);
|
||||
`,
|
||||
|
||||
colDark: css`
|
||||
@@ -55,7 +55,7 @@ const useStyle = createStyles(({ token, css }) => {
|
||||
});
|
||||
|
||||
function color2Rgba(color: string) {
|
||||
return `#${new FastColor(color).toHexString().toUpperCase()}`;
|
||||
return new FastColor(color).toHexString().toUpperCase();
|
||||
}
|
||||
|
||||
interface ColorCircleProps {
|
||||
@@ -88,16 +88,15 @@ const TokenCompare: React.FC<TokenCompareProps> = (props) => {
|
||||
const darkTokens = theme.getDesignToken({ algorithm: theme.darkAlgorithm });
|
||||
|
||||
return list.map((tokenName) => {
|
||||
const meta = tokenMeta.global[tokenName];
|
||||
const meta = (tokenMeta.global as any)[tokenName];
|
||||
const name = lang === 'cn' ? meta.name : meta.nameEn;
|
||||
|
||||
return {
|
||||
name: name.replace('颜色', '').replace('色', '').replace('Color', '').trim(),
|
||||
light: color2Rgba(lightTokens[tokenName]),
|
||||
dark: color2Rgba(darkTokens[tokenName]),
|
||||
light: color2Rgba((lightTokens as any)[tokenName]),
|
||||
dark: color2Rgba((darkTokens as any)[tokenName]),
|
||||
};
|
||||
});
|
||||
}, [tokenNames]);
|
||||
}, [lang, tokenNames]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { RightCircleOutlined } from '@ant-design/icons';
|
||||
import type { TreeGraph } from '@antv/g6';
|
||||
import { Flex } from 'antd';
|
||||
import { createStyles, css } from 'antd-style';
|
||||
import { useRouteMeta } from 'dumi';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import { renderReactToHTMLString } from '../../../theme/utils/renderReactToHTML';
|
||||
|
||||
const dataTransform = (data: BehaviorMapItem) => {
|
||||
const changeData = (d: any, level = 0) => {
|
||||
const clonedData: any = { ...d };
|
||||
interface BehaviorMapItem {
|
||||
id: string;
|
||||
label: string;
|
||||
targetType?: 'mvp' | 'extension';
|
||||
children?: BehaviorMapItem[];
|
||||
link?: string;
|
||||
collapsed?: boolean;
|
||||
type?: 'behavior-start-node' | 'behavior-sub-node';
|
||||
}
|
||||
|
||||
const dataTransform = (rootData: BehaviorMapItem) => {
|
||||
const changeData = (data: BehaviorMapItem, level = 0) => {
|
||||
const clonedData: BehaviorMapItem = { ...data };
|
||||
switch (level) {
|
||||
case 0:
|
||||
clonedData.type = 'behavior-start-node';
|
||||
@@ -19,21 +33,12 @@ const dataTransform = (data: BehaviorMapItem) => {
|
||||
clonedData.type = 'behavior-sub-node';
|
||||
break;
|
||||
}
|
||||
|
||||
if (d.children) {
|
||||
clonedData.children = d.children.map((child: any) => changeData(child, level + 1));
|
||||
if (Array.isArray(data.children)) {
|
||||
clonedData.children = data.children.map((child) => changeData(child, level + 1));
|
||||
}
|
||||
return clonedData;
|
||||
};
|
||||
return changeData(data);
|
||||
};
|
||||
|
||||
type BehaviorMapItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
targetType?: 'mvp' | 'extension';
|
||||
children?: BehaviorMapItem[];
|
||||
link?: string;
|
||||
return changeData(rootData);
|
||||
};
|
||||
|
||||
const useStyle = createStyles(({ token }) => ({
|
||||
@@ -100,16 +105,19 @@ const locales = {
|
||||
},
|
||||
};
|
||||
|
||||
export type BehaviorMapProps = {
|
||||
export interface BehaviorMapProps {
|
||||
data: BehaviorMapItem;
|
||||
};
|
||||
}
|
||||
|
||||
const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { styles } = useStyle();
|
||||
const [locale] = useLocale(locales);
|
||||
|
||||
const meta = useRouteMeta();
|
||||
|
||||
const graphRef = useRef<TreeGraph>(null);
|
||||
|
||||
useEffect(() => {
|
||||
import('@antv/g6').then((G6) => {
|
||||
G6.registerNode('behavior-start-node', {
|
||||
@@ -228,25 +236,11 @@ const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
|
||||
y: -8,
|
||||
cursor: 'pointer',
|
||||
// DOM's html
|
||||
html: `
|
||||
<div style="width: 16px; height: 16px;">
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="DatePicker" transform="translate(-890.000000, -441.000000)" fill-rule="nonzero">
|
||||
<g id="编组-30" transform="translate(288.000000, 354.000000)">
|
||||
<g id="编组-7备份-7" transform="translate(522.000000, 79.000000)">
|
||||
<g id="right-circle-outlinedd" transform="translate(80.000000, 8.000000)">
|
||||
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect>
|
||||
<path d="M10.4171875,7.8984375 L6.5734375,5.1171875 C6.490625,5.0578125 6.375,5.115625 6.375,5.21875 L6.375,5.9515625 C6.375,6.1109375 6.4515625,6.2625 6.58125,6.35625 L8.853125,8 L6.58125,9.64375 C6.4515625,9.7375 6.375,9.8875 6.375,10.0484375 L6.375,10.78125 C6.375,10.8828125 6.490625,10.9421875 6.5734375,10.8828125 L10.4171875,8.1015625 C10.4859375,8.0515625 10.4859375,7.9484375 10.4171875,7.8984375 Z" id="路径" fill="#BFBFBF"></path>
|
||||
<path d="M8,1 C4.134375,1 1,4.134375 1,8 C1,11.865625 4.134375,15 8,15 C11.865625,15 15,11.865625 15,8 C15,4.134375 11.865625,1 8,1 Z M8,13.8125 C4.790625,13.8125 2.1875,11.209375 2.1875,8 C2.1875,4.790625 4.790625,2.1875 8,2.1875 C11.209375,2.1875 13.8125,4.790625 13.8125,8 C13.8125,11.209375 11.209375,13.8125 8,13.8125 Z" id="形状" fill="#BFBFBF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
`,
|
||||
html: renderReactToHTMLString(
|
||||
<Flex align="center" justify="center">
|
||||
<RightCircleOutlined style={{ color: '#BFBFBF' }} />
|
||||
</Flex>,
|
||||
),
|
||||
},
|
||||
// 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
|
||||
name: 'sub-node-link',
|
||||
@@ -265,25 +259,11 @@ const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
|
||||
hover: {
|
||||
stroke: '#1677ff',
|
||||
'sub-node-link': {
|
||||
html: `
|
||||
<div style="width: 16px; height: 16px;">
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="DatePicker" transform="translate(-890.000000, -441.000000)" fill-rule="nonzero">
|
||||
<g id="编组-30" transform="translate(288.000000, 354.000000)">
|
||||
<g id="编组-7备份-7" transform="translate(522.000000, 79.000000)">
|
||||
<g id="right-circle-outlinedd" transform="translate(80.000000, 8.000000)">
|
||||
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect>
|
||||
<path d="M10.4171875,7.8984375 L6.5734375,5.1171875 C6.490625,5.0578125 6.375,5.115625 6.375,5.21875 L6.375,5.9515625 C6.375,6.1109375 6.4515625,6.2625 6.58125,6.35625 L8.853125,8 L6.58125,9.64375 C6.4515625,9.7375 6.375,9.8875 6.375,10.0484375 L6.375,10.78125 C6.375,10.8828125 6.490625,10.9421875 6.5734375,10.8828125 L10.4171875,8.1015625 C10.4859375,8.0515625 10.4859375,7.9484375 10.4171875,7.8984375 Z" id="路径" fill="#1677ff"></path>
|
||||
<path d="M8,1 C4.134375,1 1,4.134375 1,8 C1,11.865625 4.134375,15 8,15 C11.865625,15 15,11.865625 15,8 C15,4.134375 11.865625,1 8,1 Z M8,13.8125 C4.790625,13.8125 2.1875,11.209375 2.1875,8 C2.1875,4.790625 4.790625,2.1875 8,2.1875 C11.209375,2.1875 13.8125,4.790625 13.8125,8 C13.8125,11.209375 11.209375,13.8125 8,13.8125 Z" id="形状" fill="#1677ff"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
`,
|
||||
html: renderReactToHTMLString(
|
||||
<Flex align="center" justify="center">
|
||||
<RightCircleOutlined style={{ color: '#1677ff' }} />
|
||||
</Flex>,
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -291,7 +271,7 @@ const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
|
||||
},
|
||||
'rect',
|
||||
);
|
||||
const graph = new G6.TreeGraph({
|
||||
graphRef.current = new G6.TreeGraph({
|
||||
container: ref.current!,
|
||||
width: ref.current!.scrollWidth,
|
||||
height: ref.current!.scrollHeight,
|
||||
@@ -301,10 +281,7 @@ const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
|
||||
},
|
||||
defaultEdge: {
|
||||
type: 'cubic-horizontal',
|
||||
style: {
|
||||
lineWidth: 1,
|
||||
stroke: '#BFBFBF',
|
||||
},
|
||||
style: { lineWidth: 1, stroke: '#BFBFBF' },
|
||||
},
|
||||
layout: {
|
||||
type: 'mindmap',
|
||||
@@ -317,24 +294,26 @@ const BehaviorMap: React.FC<BehaviorMapProps> = ({ data }) => {
|
||||
},
|
||||
});
|
||||
|
||||
graph.on('node:mouseenter', (e) => {
|
||||
graph.setItemState(e.item!, 'hover', true);
|
||||
graphRef.current?.on('node:mouseenter', (e) => {
|
||||
graphRef.current?.setItemState(e.item!, 'hover', true);
|
||||
});
|
||||
graph.on('node:mouseleave', (e) => {
|
||||
graph.setItemState(e.item!, 'hover', false);
|
||||
graphRef.current?.on('node:mouseleave', (e) => {
|
||||
graphRef.current?.setItemState(e.item!, 'hover', false);
|
||||
});
|
||||
graph.on('node:click', (e) => {
|
||||
graphRef.current?.on('node:click', (e) => {
|
||||
const { link } = e.item!.getModel();
|
||||
if (link) {
|
||||
window.location.hash = link as string;
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(dataTransform(data));
|
||||
graph.render();
|
||||
graph.fitCenter();
|
||||
graphRef.current?.data(dataTransform(data));
|
||||
graphRef.current?.render();
|
||||
graphRef.current?.fitCenter();
|
||||
});
|
||||
}, []);
|
||||
return () => {
|
||||
graphRef.current?.destroy();
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={styles.container}>
|
||||
|
||||
@@ -27,7 +27,12 @@ const BezierVisualizer = (props: BezierVisualizerProps) => {
|
||||
const controls = useMemo(() => {
|
||||
const m = RE.exec(value.toLowerCase().trim());
|
||||
if (m) {
|
||||
return m[1].split(',').map((v) => parseFloat(v.trim())) as [number, number, number, number];
|
||||
return m[1].split(',').map((v) => Number.parseFloat(v.trim())) as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}, [value]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Button, Tabs, Typography } from 'antd';
|
||||
import { Tabs, Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import toReactElement from 'jsonml-to-react-element';
|
||||
import JsonML from 'jsonml.js/lib/utils';
|
||||
@@ -109,12 +109,16 @@ const CodePreview: React.FC<CodePreviewProps> = ({
|
||||
}
|
||||
const [highlightedCodes, setHighlightedCodes] = React.useState(initialCodes);
|
||||
const { codeType, setCodeType } = React.use(DemoContext);
|
||||
const sourceCodes = {
|
||||
// omit trailing line break
|
||||
tsx: sourceCode?.trim(),
|
||||
jsx: jsxCode?.trim(),
|
||||
style: styleCode?.trim(),
|
||||
} as Record<'tsx' | 'jsx' | 'style', string>;
|
||||
|
||||
const sourceCodes = useMemo<Record<'tsx' | 'jsx' | 'style', string>>(() => {
|
||||
return {
|
||||
// omit trailing line break
|
||||
tsx: sourceCode?.trim(),
|
||||
jsx: jsxCode?.trim(),
|
||||
style: styleCode?.trim(),
|
||||
};
|
||||
}, [sourceCode, jsxCode, styleCode]);
|
||||
|
||||
useEffect(() => {
|
||||
const codes = {
|
||||
tsx: Prism.highlight(sourceCode, Prism.languages.javascript, 'jsx'),
|
||||
@@ -153,13 +157,24 @@ const CodePreview: React.FC<CodePreviewProps> = ({
|
||||
) : (
|
||||
toReactComponent(['pre', { lang, highlighted: highlightedCodes[lang] }])
|
||||
)}
|
||||
<Button type="text" className={styles.copyButton}>
|
||||
{/* button 嵌套 button 会导致水合失败,这里需要用 div 标签,不能用 button */}
|
||||
<div className={styles.copyButton}>
|
||||
<Typography.Text className={styles.copyIcon} copyable={{ text: sourceCodes[lang] }} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
[JSON.stringify(highlightedCodes), styles.code, styles.copyButton, styles.copyIcon],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
entryName,
|
||||
error,
|
||||
highlightedCodes,
|
||||
langList,
|
||||
sourceCodes,
|
||||
styles.code,
|
||||
styles.copyButton,
|
||||
styles.copyIcon,
|
||||
],
|
||||
);
|
||||
|
||||
if (!langList.length) {
|
||||
|
||||
@@ -45,7 +45,7 @@ const ColorPaletteTool: React.FC = () => {
|
||||
}
|
||||
}
|
||||
return <span className="color-palette-picker-validation">{text.trim()}</span>;
|
||||
}, [primaryColorInstance, primaryMinSaturation, primaryMinBrightness]);
|
||||
}, [primaryColorInstance, locale]);
|
||||
return (
|
||||
<div className="color-palette-horizontal">
|
||||
<div className="color-palette-pick">
|
||||
|
||||
@@ -54,7 +54,7 @@ const ColorPaletteTool: React.FC = () => {
|
||||
{text.trim()}
|
||||
</span>
|
||||
);
|
||||
}, [primaryColorInstance]);
|
||||
}, [locale, primaryColorInstance]);
|
||||
|
||||
return (
|
||||
<div className="color-palette-horizontal color-palette-horizontal-dark">
|
||||
|
||||
@@ -1,89 +1,115 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import Palette from './Palette';
|
||||
|
||||
const colors = [
|
||||
{
|
||||
name: 'red',
|
||||
english: 'Dust Red',
|
||||
chinese: '薄暮',
|
||||
description: '斗志、奔放',
|
||||
const COLOR_KEYS = [
|
||||
'red',
|
||||
'volcano',
|
||||
'orange',
|
||||
'lime',
|
||||
'gold',
|
||||
'yellow',
|
||||
'green',
|
||||
'cyan',
|
||||
'blue',
|
||||
'geekblue',
|
||||
'purple',
|
||||
'magenta',
|
||||
] as const;
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
redTitle: '薄暮',
|
||||
redDescription: '斗志、奔放',
|
||||
|
||||
volcanoTitle: '火山',
|
||||
volcanoDescription: '醒目、澎湃',
|
||||
|
||||
orangeTitle: '日暮',
|
||||
orangeDescription: '温暖、欢快',
|
||||
|
||||
limeTitle: '青柠',
|
||||
limeDescription: '自然、生机',
|
||||
|
||||
goldTitle: '金盏花',
|
||||
goldDescription: '活力、积极',
|
||||
|
||||
yellowTitle: '日出',
|
||||
yellowDescription: '出生、阳光',
|
||||
|
||||
greenTitle: '极光绿',
|
||||
greenDescription: '健康、创新',
|
||||
|
||||
cyanTitle: '明青',
|
||||
cyanDescription: '希望、坚强',
|
||||
|
||||
blueTitle: '拂晓蓝',
|
||||
blueDescription: '包容、科技、普惠',
|
||||
|
||||
geekblueTitle: '极客蓝',
|
||||
geekblueDescription: '探索、钻研',
|
||||
|
||||
purpleTitle: '酱紫',
|
||||
purpleDescription: '优雅、浪漫',
|
||||
|
||||
magentaTitle: '法式洋红',
|
||||
magentaDescription: '明快、感性',
|
||||
},
|
||||
{
|
||||
name: 'volcano',
|
||||
english: 'Volcano',
|
||||
chinese: '火山',
|
||||
description: '醒目、澎湃',
|
||||
en: {
|
||||
redTitle: 'Dust Red',
|
||||
redDescription: 'Fighting Spirit, Unrestrained',
|
||||
|
||||
volcanoTitle: 'Volcano',
|
||||
volcanoDescription: 'Eye-catching, Surging',
|
||||
|
||||
orangeTitle: 'Sunset Orange',
|
||||
orangeDescription: 'Warm, Cheerful',
|
||||
|
||||
limeTitle: 'Lime',
|
||||
limeDescription: 'Natural, Vitality',
|
||||
|
||||
goldTitle: 'Calendula Gold',
|
||||
goldDescription: 'Energetic, Positive',
|
||||
|
||||
yellowTitle: 'Sunrise Yellow',
|
||||
yellowDescription: 'Birth, Sunshine',
|
||||
|
||||
greenTitle: 'Polar Green',
|
||||
greenDescription: 'Healthy, Innovative',
|
||||
|
||||
cyanTitle: 'Cyan',
|
||||
cyanDescription: 'Hope, Strong',
|
||||
|
||||
blueTitle: 'Daybreak Blue',
|
||||
blueDescription: 'Inclusive, Technology, Universal',
|
||||
|
||||
geekblueTitle: 'Geek Blue',
|
||||
geekblueDescription: 'Exploration, Research',
|
||||
|
||||
purpleTitle: 'Golden Purple',
|
||||
purpleDescription: 'Elegant, Romantic',
|
||||
|
||||
magentaTitle: 'French Magenta',
|
||||
magentaDescription: 'Bright, Emotional',
|
||||
},
|
||||
{
|
||||
name: 'orange',
|
||||
english: 'Sunset Orange',
|
||||
chinese: '日暮',
|
||||
description: '温暖、欢快',
|
||||
},
|
||||
{
|
||||
name: 'gold',
|
||||
english: 'Calendula Gold',
|
||||
chinese: '金盏花',
|
||||
description: '活力、积极',
|
||||
},
|
||||
{
|
||||
name: 'yellow',
|
||||
english: 'Sunrise Yellow',
|
||||
chinese: '日出',
|
||||
description: '出生、阳光',
|
||||
},
|
||||
{
|
||||
name: 'lime',
|
||||
english: 'Lime',
|
||||
chinese: '青柠',
|
||||
description: '自然、生机',
|
||||
},
|
||||
{
|
||||
name: 'green',
|
||||
english: 'Polar Green',
|
||||
chinese: '极光绿',
|
||||
description: '健康、创新',
|
||||
},
|
||||
{
|
||||
name: 'cyan',
|
||||
english: 'Cyan',
|
||||
chinese: '明青',
|
||||
description: '希望、坚强',
|
||||
},
|
||||
{
|
||||
name: 'blue',
|
||||
english: 'Daybreak Blue',
|
||||
chinese: '拂晓蓝',
|
||||
description: '包容、科技、普惠',
|
||||
},
|
||||
{
|
||||
name: 'geekblue',
|
||||
english: 'Geek Blue',
|
||||
chinese: '极客蓝',
|
||||
description: '探索、钻研',
|
||||
},
|
||||
{
|
||||
name: 'purple',
|
||||
english: 'Golden Purple',
|
||||
chinese: '酱紫',
|
||||
description: '优雅、浪漫',
|
||||
},
|
||||
{
|
||||
name: 'magenta',
|
||||
english: 'Magenta',
|
||||
chinese: '法式洋红',
|
||||
description: '明快、感性',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const ColorPalettes: React.FC<{ dark?: boolean }> = (props) => {
|
||||
const { dark } = props;
|
||||
const [locale] = useLocale(locales);
|
||||
const memoizedColors = useMemo(() => {
|
||||
return COLOR_KEYS.map((key) => ({
|
||||
name: key,
|
||||
title: locale[`${key}Title`],
|
||||
description: locale[`${key}Description`],
|
||||
}));
|
||||
}, [locale]);
|
||||
return (
|
||||
<div className={classNames('color-palettes', { 'color-palettes-dark': dark })}>
|
||||
{colors.map((color) => (
|
||||
<Palette key={color.name} color={color} dark={dark} showTitle />
|
||||
{memoizedColors.map((color) => (
|
||||
<Palette key={`item-${color.name}`} color={color} dark={dark} showTitle />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ interface ColorPatternsProps {
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
const ColorPatterns: React.FC<ColorPatternsProps> = ({ color, dark, backgroundColor }) => {
|
||||
const ColorPatterns: React.FC<ColorPatternsProps> = ({ color = '', dark, backgroundColor }) => {
|
||||
const colors = generate(color, dark ? { theme: 'dark', backgroundColor } : {});
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -8,9 +8,9 @@ const rgbToHex = (rgbString: string): string => {
|
||||
if (!rgb) {
|
||||
return '';
|
||||
}
|
||||
let r = parseInt(rgb[0], 10).toString(16);
|
||||
let g = parseInt(rgb[1], 10).toString(16);
|
||||
let b = parseInt(rgb[2], 10).toString(16);
|
||||
let r = Number.parseInt(rgb[0], 10).toString(16);
|
||||
let g = Number.parseInt(rgb[1], 10).toString(16);
|
||||
let b = Number.parseInt(rgb[2], 10).toString(16);
|
||||
r = r.length === 1 ? `0${r}` : r;
|
||||
g = g.length === 1 ? `0${g}` : g;
|
||||
b = b.length === 1 ? `0${b}` : b;
|
||||
@@ -21,22 +21,19 @@ interface PaletteProps {
|
||||
showTitle?: boolean;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
dark?: boolean;
|
||||
count?: number;
|
||||
color?: {
|
||||
name: string;
|
||||
count?: number;
|
||||
name?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
english?: string;
|
||||
chinese?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const Palette: React.FC<PaletteProps> = (props) => {
|
||||
const {
|
||||
showTitle,
|
||||
direction,
|
||||
dark,
|
||||
color: { name, count = 10, description, english, chinese } = { name: 'gray', count: 13 },
|
||||
} = props;
|
||||
const { showTitle, direction, dark, count = 10, color = {} } = props;
|
||||
|
||||
const { name = 'gray', title, description } = color;
|
||||
|
||||
const [hexColors, setHexColors] = React.useState<Record<PropertyKey, string>>({});
|
||||
const colorNodesRef = React.useRef<Record<PropertyKey, HTMLDivElement>>({});
|
||||
const { message } = App.useApp();
|
||||
@@ -44,28 +41,28 @@ const Palette: React.FC<PaletteProps> = (props) => {
|
||||
useEffect(() => {
|
||||
const colors: Record<string, string> = {};
|
||||
Object.keys(colorNodesRef.current || {}).forEach((key) => {
|
||||
const computedColor = getComputedStyle(colorNodesRef.current[key])['background-color'];
|
||||
if (computedColor.includes('rgba')) {
|
||||
colors[key] = computedColor;
|
||||
const { backgroundColor } = getComputedStyle(colorNodesRef.current[key]);
|
||||
if (backgroundColor.includes('rgba')) {
|
||||
colors[key] = backgroundColor;
|
||||
} else {
|
||||
colors[key] = rgbToHex(computedColor);
|
||||
colors[key] = rgbToHex(backgroundColor);
|
||||
}
|
||||
});
|
||||
setHexColors(colors);
|
||||
}, []);
|
||||
|
||||
const className = direction === 'horizontal' ? 'color-palette-horizontal' : 'color-palette';
|
||||
const colors: React.ReactNode[] = [];
|
||||
const colorName = `${english} / ${chinese}`;
|
||||
|
||||
const colorPaletteMap = {
|
||||
dark: ['#fff', 'unset'],
|
||||
default: ['rgba(0, 0, 0, 0.85)', '#fff'],
|
||||
};
|
||||
const [lastColor, firstColor] = dark ? colorPaletteMap.dark : colorPaletteMap.default;
|
||||
for (let i = 1; i <= count; i += 1) {
|
||||
const colorText = `${name}-${i}`;
|
||||
const defaultBgStyle = dark ? presetDarkPalettes[name][i - 1] : '';
|
||||
colors.push(
|
||||
|
||||
const colors: React.ReactNode[] = Array.from({ length: count }, (_, i) => {
|
||||
const colorText = `${name}-${i + 1}`;
|
||||
const defaultBgStyle = dark && name ? presetDarkPalettes[name][i] : '';
|
||||
return (
|
||||
<CopyToClipboard
|
||||
text={hexColors[colorText]}
|
||||
onCopy={() => message.success(`@${colorText} copied: ${hexColors[colorText]}`)}
|
||||
@@ -89,15 +86,16 @@ const Palette: React.FC<PaletteProps> = (props) => {
|
||||
<span className="main-color-text">{colorText}</span>
|
||||
<span className="main-color-value">{hexColors[colorText]}</span>
|
||||
</div>
|
||||
</CopyToClipboard>,
|
||||
</CopyToClipboard>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{showTitle && (
|
||||
<div className="color-title">
|
||||
{colorName}
|
||||
<span className="color-description">{description}</span>
|
||||
{title}
|
||||
{description && <span className="color-description">{description}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="main-color">{colors}</div>
|
||||
|
||||
@@ -3,8 +3,8 @@ import { BugOutlined } from '@ant-design/icons';
|
||||
import { Button, Drawer, Flex, Grid, Popover, Tag, Timeline, Typography } from 'antd';
|
||||
import type { TimelineItemProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import useFetch from '../../../hooks/useFetch';
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import useLocation from '../../../hooks/useLocation';
|
||||
import { matchDeprecated } from '../../utils';
|
||||
@@ -189,6 +189,7 @@ const RenderChangelogList: React.FC<{ changelogList: ChangelogInfo[] }> = ({ cha
|
||||
<RefLinks refs={refs} contributors={contributors} />
|
||||
<br />
|
||||
<img
|
||||
draggable={false}
|
||||
src={imgElement?.getAttribute('src') || ''}
|
||||
alt={imgElement?.getAttribute('alt') || ''}
|
||||
width={imgElement?.getAttribute('width') || ''}
|
||||
@@ -211,17 +212,23 @@ const RenderChangelogList: React.FC<{ changelogList: ChangelogInfo[] }> = ({ cha
|
||||
const useChangelog = (componentPath: string, lang: 'cn' | 'en'): ChangelogInfo[] => {
|
||||
const logFileName = `components-changelog-${lang}.json`;
|
||||
|
||||
const data = useFetch({
|
||||
key: `component-changelog-${lang}`,
|
||||
request: () => import(`../../../preset/${logFileName}`),
|
||||
});
|
||||
return React.useMemo(() => {
|
||||
const component = componentPath.replace(/-/g, '');
|
||||
const componentName = Object.keys(data).find(
|
||||
(name) => name.toLowerCase() === component.toLowerCase(),
|
||||
);
|
||||
return data[componentName as keyof typeof data] as ChangelogInfo[];
|
||||
}, [data, componentPath]);
|
||||
const { data, error, isLoading } = useSWR(
|
||||
`component-changelog-${lang}`,
|
||||
() => import(`../../../preset/${logFileName}`),
|
||||
);
|
||||
|
||||
if (error || isLoading) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const component = componentPath.replace(/-/g, '');
|
||||
const componentName = Object.keys(data).find(
|
||||
(name) => name.toLowerCase() === component.toLowerCase(),
|
||||
);
|
||||
if (!componentName) {
|
||||
return [];
|
||||
}
|
||||
return data?.[componentName] || [];
|
||||
};
|
||||
|
||||
const ComponentChangelog: React.FC<Readonly<React.PropsWithChildren>> = (props) => {
|
||||
@@ -294,7 +301,17 @@ const ComponentChangelog: React.FC<Readonly<React.PropsWithChildren>> = (props)
|
||||
),
|
||||
};
|
||||
});
|
||||
}, [list]);
|
||||
}, [
|
||||
lang,
|
||||
list,
|
||||
locale.bugList,
|
||||
styles.bug,
|
||||
styles.bugReasonList,
|
||||
styles.bugReasonTitle,
|
||||
styles.versionTag,
|
||||
styles.versionTitle,
|
||||
styles.versionWrap,
|
||||
]);
|
||||
|
||||
const screens = Grid.useBreakpoint();
|
||||
const width = screens.md ? '48vw' : '90vw';
|
||||
|
||||
@@ -10,9 +10,7 @@ const Editor: React.FC<JSONEditorPropsOptional> = (props) => {
|
||||
if (container.current) {
|
||||
editorRef.current = createJSONEditor({
|
||||
target: container.current,
|
||||
props: {
|
||||
mode: Mode.text,
|
||||
},
|
||||
props: { mode: Mode.text },
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
@@ -22,7 +20,7 @@ const Editor: React.FC<JSONEditorPropsOptional> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
editorRef.current?.updateProps(props);
|
||||
}, [props.content]);
|
||||
}, [props]);
|
||||
|
||||
return <div ref={container} className="vanilla-jsoneditor-react" />;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MouseEvent, MouseEventHandler } from 'react';
|
||||
import React, { useMemo, forwardRef } from 'react';
|
||||
import { Link as DumiLink, useLocation, useAppData, useNavigate } from 'dumi';
|
||||
import React, { forwardRef, useMemo } from 'react';
|
||||
import { Link as DumiLink, useAppData, useLocation, useNavigate } from 'dumi';
|
||||
|
||||
export interface LinkProps {
|
||||
to: string | { pathname?: string; search?: string; hash?: string };
|
||||
@@ -21,7 +21,7 @@ const Link = forwardRef<HTMLAnchorElement, React.PropsWithChildren<LinkProps>>(
|
||||
return `${to.pathname || pathname}${to.search || ''}${to.hash || ''}`;
|
||||
}
|
||||
return to;
|
||||
}, [to]);
|
||||
}, [pathname, to]);
|
||||
const onClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
||||
rest.onClick?.(e);
|
||||
if (!href?.startsWith('http')) {
|
||||
|
||||
@@ -16,6 +16,7 @@ const Loading: React.FC = () => {
|
||||
width={40}
|
||||
height={40}
|
||||
alt="loading"
|
||||
draggable={false}
|
||||
style={{ marginBottom: 24, filter: 'grayscale(1)', opacity: 0.33 }}
|
||||
/>
|
||||
<Skeleton active paragraph={{ rows: 3 }} />
|
||||
|
||||
@@ -55,7 +55,7 @@ const Markers: React.FC<MarkersProps> = (props) => {
|
||||
},
|
||||
);
|
||||
});
|
||||
}, [targetClassName]);
|
||||
}, [containerRef, targetClassName]);
|
||||
|
||||
// ======================== Render =========================
|
||||
return (
|
||||
|
||||
@@ -33,9 +33,7 @@ const Block: React.FC<BlockProps> = ({ component: Component, options, defaultVal
|
||||
defaultValue={defaultValue}
|
||||
getPopupContainer={() => divRef.current}
|
||||
options={options}
|
||||
styles={{
|
||||
popup: { zIndex: 1 },
|
||||
}}
|
||||
styles={{ popup: { zIndex: 1 } }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ const useStyle = createStyles(({ token }) => ({
|
||||
position: relative;
|
||||
`,
|
||||
colWrap: css`
|
||||
border-right: 1px solid ${token.colorBorderSecondary};
|
||||
border-inline-end: 1px solid ${token.colorBorderSecondary};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -101,7 +101,7 @@ function HighlightExample(props: {
|
||||
}
|
||||
|
||||
return Prism.highlight(code, Prism.languages.javascript, 'jsx');
|
||||
}, [componentName, semanticName]);
|
||||
}, [componentName, itemsAPI, semanticName]);
|
||||
|
||||
return (
|
||||
// biome-ignore lint: lint/security/noDangerouslySetInnerHtml
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React, { use, useRef } from 'react';
|
||||
import { BgColorsOutlined, LinkOutlined, SmileOutlined, SunOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
BgColorsOutlined,
|
||||
LinkOutlined,
|
||||
SmileOutlined,
|
||||
SunOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Badge, Button, Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { CompactTheme, DarkTheme } from 'antd-token-previewer/es/icons';
|
||||
@@ -8,11 +14,14 @@ import { FormattedMessage, useLocation } from 'dumi';
|
||||
import useThemeAnimation from '../../../hooks/useThemeAnimation';
|
||||
import type { SiteContextProps } from '../../slots/SiteContext';
|
||||
import SiteContext from '../../slots/SiteContext';
|
||||
import { getLocalizedPathname, isZhCN } from '../../utils';
|
||||
import { getLocalizedPathname, isZhCN, isLocalStorageNameSupported } from '../../utils';
|
||||
import Link from '../Link';
|
||||
import ThemeIcon from './ThemeIcon';
|
||||
|
||||
export type ThemeName = 'light' | 'dark' | 'compact' | 'motion-off' | 'happy-work';
|
||||
export type ThemeName = 'light' | 'dark' | 'auto' | 'compact' | 'motion-off' | 'happy-work';
|
||||
|
||||
// 主题持久化存储键名
|
||||
const ANT_DESIGN_SITE_THEME = 'ant-design-site-theme';
|
||||
|
||||
export interface ThemeSwitchProps {
|
||||
value?: ThemeName[];
|
||||
@@ -29,10 +38,16 @@ const ThemeSwitch: React.FC<ThemeSwitchProps> = () => {
|
||||
// 主题选项配置
|
||||
const themeOptions = [
|
||||
{
|
||||
id: 'app.theme.switch.default',
|
||||
id: 'app.theme.switch.auto',
|
||||
icon: <SyncOutlined />,
|
||||
key: 'auto',
|
||||
showBadge: () => theme.includes('auto'),
|
||||
},
|
||||
{
|
||||
id: 'app.theme.switch.light',
|
||||
icon: <SunOutlined />,
|
||||
key: 'light',
|
||||
showBadge: () => theme.includes('light') || theme.length === 0,
|
||||
showBadge: () => theme.includes('light'),
|
||||
},
|
||||
{
|
||||
id: 'app.theme.switch.dark',
|
||||
@@ -102,21 +117,30 @@ const ThemeSwitch: React.FC<ThemeSwitchProps> = () => {
|
||||
|
||||
const themeKey = key as ThemeName;
|
||||
|
||||
// 亮色/暗色模式是互斥的
|
||||
if (['light', 'dark'].includes(key)) {
|
||||
// 亮色/暗色/自动模式是互斥的
|
||||
if (['light', 'dark', 'auto'].includes(key)) {
|
||||
// 校验当前主题是否包含要切换的主题(避免 timeout in DOM update)
|
||||
if (theme.includes(themeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 亮色/暗色模式切换时应用动画效果
|
||||
lastThemeKey.current = key;
|
||||
toggleAnimationTheme(domEvent, theme.includes('dark'));
|
||||
if (['light', 'dark'].includes(key)) {
|
||||
lastThemeKey.current = key;
|
||||
toggleAnimationTheme(domEvent, theme.includes('dark'));
|
||||
}
|
||||
|
||||
const filteredTheme = theme.filter((t) => !['light', 'dark', 'auto'].includes(t));
|
||||
const newTheme = [...filteredTheme, themeKey];
|
||||
|
||||
const filteredTheme = theme.filter((t) => !['light', 'dark'].includes(t));
|
||||
updateSiteConfig({
|
||||
theme: [...filteredTheme, themeKey],
|
||||
theme: newTheme,
|
||||
});
|
||||
|
||||
// 持久化到 localStorage
|
||||
if (isLocalStorageNameSupported()) {
|
||||
localStorage.setItem(ANT_DESIGN_SITE_THEME, themeKey);
|
||||
}
|
||||
} else {
|
||||
// 其他主题选项是开关式的
|
||||
const hasTheme = theme.includes(themeKey);
|
||||
|
||||
@@ -18,7 +18,7 @@ const GlobalStyle: React.FC = () => {
|
||||
font-size: ${token.fontSize}px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
|
||||
.highlight {
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -208,13 +208,13 @@ const GlobalStyle: React.FC = () => {
|
||||
padding-inline-start: 0.8em;
|
||||
color: ${token.colorTextSecondary};
|
||||
font-size: 90%;
|
||||
border-left: 4px solid ${token.colorSplit};
|
||||
border-inline-start: 4px solid ${token.colorSplit};
|
||||
|
||||
.rtl & {
|
||||
padding-inline-end: 0.8em;
|
||||
padding-inline-start: 0;
|
||||
border-right: 4px solid ${token.colorSplit};
|
||||
border-left: none;
|
||||
border-inline-end: 4px solid ${token.colorSplit};
|
||||
border-inline-start: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,11 +298,11 @@ const GlobalStyle: React.FC = () => {
|
||||
border: 1px solid ${token.colorSplit};
|
||||
|
||||
&:first-child {
|
||||
border-left: 1px solid ${token.colorSplit};
|
||||
border-inline-start: 1px solid ${token.colorSplit};
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid ${token.colorSplit};
|
||||
border-inline-end: 1px solid ${token.colorSplit};
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -381,23 +381,23 @@ const GlobalStyle: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Api 表中某些属性用 del 标记,表示已废弃(但仍期望给开发者一个过渡期)用 css 标记出来。仅此而已。
|
||||
有更多看法?移步讨论区: https://github.com/ant-design/ant-design/discussions/51298
|
||||
*/
|
||||
tr:has(td:first-child > del) {
|
||||
color: ${token.colorWarning} !important;
|
||||
background-color: ${token.colorWarningBg} !important;
|
||||
display: var(--antd-site-api-deprecated-display, none);
|
||||
tr:has(td:first-child > del) {
|
||||
color: ${token.colorWarning} !important;
|
||||
background-color: ${token.colorWarningBg} !important;
|
||||
display: var(--antd-site-api-deprecated-display, none);
|
||||
|
||||
del {
|
||||
color: ${token.colorWarning};
|
||||
}
|
||||
|
||||
&:hover del {
|
||||
text-decoration: none;
|
||||
}
|
||||
del {
|
||||
color: ${token.colorWarning};
|
||||
}
|
||||
|
||||
&:hover del {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-demo,
|
||||
|
||||
@@ -20,7 +20,7 @@ export default () => {
|
||||
|
||||
.spinner-icon {
|
||||
border-top-color: ${token.colorPrimary};
|
||||
border-left-color: ${token.colorPrimary};
|
||||
border-inline-start-color: ${token.colorPrimary};
|
||||
}
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -9,67 +9,55 @@ export default () => {
|
||||
<Global
|
||||
styles={css`
|
||||
@font-face {
|
||||
font-family: "AlibabaSans";
|
||||
font-family: 'AlibabaSans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*1GSgSYDD_aIAAAAAQsAAAAgAegCCAQ/AlibabaSans-Light.woff2")
|
||||
format("woff2");
|
||||
src: url('//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*1GSgSYDD_aIAAAAAQsAAAAgAegCCAQ/AlibabaSans-Light.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: "AlibabaSans";
|
||||
font-family: 'AlibabaSans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*2zEUQqnPNesAAAAAQtAAAAgAegCCAQ/AlibabaSans-Regular.woff2")
|
||||
format("woff2");
|
||||
src: url('//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*2zEUQqnPNesAAAAAQtAAAAgAegCCAQ/AlibabaSans-Regular.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: "AlibabaSans";
|
||||
font-family: 'AlibabaSans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*E_cxRbMlZqUAAAAAQuAAAAgAegCCAQ/AlibabaSans-Medium.woff2")
|
||||
format("woff2");
|
||||
src: url('//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*E_cxRbMlZqUAAAAAQuAAAAgAegCCAQ/AlibabaSans-Medium.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: "AlibabaSans";
|
||||
font-family: 'AlibabaSans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*E_cxRbMlZqUAAAAAQuAAAAgAegCCAQ/AlibabaSans-Bold.woff2")
|
||||
format("woff2");
|
||||
src: url('//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*E_cxRbMlZqUAAAAAQuAAAAgAegCCAQ/AlibabaSans-Bold.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: "AlibabaSans";
|
||||
font-family: 'AlibabaSans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*E_cxRbMlZqUAAAAAQuAAAAgAegCCAQ/AlibabaSans-Heavy.woff2")
|
||||
format("woff2");
|
||||
src: url('//mdn.alipayobjects.com/huamei_iwk9zp/afts/file/A*E_cxRbMlZqUAAAAAQuAAAAgAegCCAQ/AlibabaSans-Heavy.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
|
||||
html {
|
||||
direction: initial;
|
||||
|
||||
@supports (overflow-x: clip) {
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
&.rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@supports (overflow-x: clip) {
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
@supports not (overflow-x: clip) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
overflow-x: hidden;
|
||||
color: ${token.colorText};
|
||||
font-size: ${token.fontSize}px;
|
||||
font-family: ${token.fontFamily};
|
||||
|
||||
@@ -66,7 +66,7 @@ export default () => {
|
||||
float: none;
|
||||
width: auto;
|
||||
padding-bottom: 30px;
|
||||
border-right: 0;
|
||||
border-inline-end: 0;
|
||||
}
|
||||
|
||||
.ant-row-rtl {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export { default as HeadingAnchor } from './HeadingAnchor';
|
||||
export { default as Reset } from './Reset';
|
||||
export { default as Common } from './Common';
|
||||
export { default as Markdown } from './Markdown';
|
||||
export { default as Highlight } from './Highlight';
|
||||
export { default as Demo } from './Demo';
|
||||
export { default as Responsive } from './Responsive';
|
||||
export { default as HeadingAnchor } from './HeadingAnchor';
|
||||
export { default as Highlight } from './Highlight';
|
||||
export { default as Markdown } from './Markdown';
|
||||
export { default as NProgress } from './NProgress';
|
||||
export { default as PreviewImage } from './PreviewImage';
|
||||
export { default as Reset } from './Reset';
|
||||
export { default as Responsive } from './Responsive';
|
||||
export { default as SearchBar } from './SearchBar';
|
||||
|
||||
@@ -6,14 +6,13 @@ import 'dayjs/locale/zh-cn';
|
||||
import React, { useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
import zhCN from 'antd/es/locale/zh_CN';
|
||||
import { Helmet, useOutlet, useSiteData } from 'dumi';
|
||||
import { Helmet, useOutlet, useSearchParams, useSiteData } from 'dumi';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import useLocation from '../../../hooks/useLocation';
|
||||
import GlobalStyles from '../../common/GlobalStyles';
|
||||
import Header from '../../slots/Header';
|
||||
import SiteContext from '../../slots/SiteContext';
|
||||
|
||||
import IndexLayout from '../IndexLayout';
|
||||
import ResourceLayout from '../ResourceLayout';
|
||||
import SidebarLayout from '../SidebarLayout';
|
||||
@@ -39,6 +38,8 @@ const DocLayout: React.FC = () => {
|
||||
const { direction } = React.use(SiteContext);
|
||||
const { loading } = useSiteData();
|
||||
const { token } = theme.useToken();
|
||||
const [searchParams] = useSearchParams();
|
||||
const hideLayout = searchParams.get('layout') === 'false';
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (lang === 'cn') {
|
||||
@@ -46,7 +47,7 @@ const DocLayout: React.FC = () => {
|
||||
} else {
|
||||
dayjs.locale('en');
|
||||
}
|
||||
}, []);
|
||||
}, [lang]);
|
||||
|
||||
useEffect(() => {
|
||||
const nprogressHiddenStyle = document.getElementById('nprogress-style');
|
||||
@@ -68,13 +69,10 @@ const DocLayout: React.FC = () => {
|
||||
if (typeof (window as any).ga !== 'undefined') {
|
||||
(window as any).ga('send', 'pageview', pathname + search);
|
||||
}
|
||||
}, [location]);
|
||||
}, [pathname, search]);
|
||||
|
||||
const content = React.useMemo<React.ReactNode>(() => {
|
||||
if (
|
||||
['', '/'].some((path) => path === pathname) ||
|
||||
['/index'].some((path) => pathname.startsWith(path))
|
||||
) {
|
||||
if (['', '/'].includes(pathname) || ['/index'].some((path) => pathname.startsWith(path))) {
|
||||
return (
|
||||
<IndexLayout title={locale.title} desc={locale.description}>
|
||||
{outlet}
|
||||
@@ -88,7 +86,7 @@ const DocLayout: React.FC = () => {
|
||||
return outlet;
|
||||
}
|
||||
return <SidebarLayout>{outlet}</SidebarLayout>;
|
||||
}, [pathname, outlet]);
|
||||
}, [pathname, outlet, locale.title, locale.description]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -112,14 +110,10 @@ const DocLayout: React.FC = () => {
|
||||
<ConfigProvider
|
||||
direction={direction}
|
||||
locale={lang === 'cn' ? zhCN : undefined}
|
||||
theme={{
|
||||
token: {
|
||||
fontFamily: `AlibabaSans, ${token.fontFamily}`,
|
||||
},
|
||||
}}
|
||||
theme={{ token: { fontFamily: `AlibabaSans, ${token.fontFamily}` } }}
|
||||
>
|
||||
<GlobalStyles />
|
||||
<Header />
|
||||
{!hideLayout && <Header />}
|
||||
{content}
|
||||
</ConfigProvider>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// prettier-ignore
|
||||
import { scan } from 'react-scan'; // import this BEFORE react
|
||||
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import {
|
||||
createCache,
|
||||
@@ -23,6 +20,7 @@ import type { ThemeName } from '../common/ThemeSwitch';
|
||||
import SiteThemeProvider from '../SiteThemeProvider';
|
||||
import type { SiteContextProps } from '../slots/SiteContext';
|
||||
import SiteContext from '../slots/SiteContext';
|
||||
import { isLocalStorageNameSupported } from '../utils';
|
||||
|
||||
import '@ant-design/v5-patch-for-react-19';
|
||||
|
||||
@@ -32,12 +30,8 @@ type SiteState = Partial<Omit<SiteContextProps, 'updateSiteConfig'>>;
|
||||
const RESPONSIVE_MOBILE = 768;
|
||||
export const ANT_DESIGN_NOT_SHOW_BANNER = 'ANT_DESIGN_NOT_SHOW_BANNER';
|
||||
|
||||
const getSystemTheme = (): 'dark' | 'light' => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'light';
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
};
|
||||
// 主题持久化存储键名
|
||||
const ANT_DESIGN_SITE_THEME = 'ant-design-site-theme';
|
||||
|
||||
// Compatible with old anchors
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -47,13 +41,6 @@ if (typeof window !== 'undefined') {
|
||||
location.hash = `#${hashId.replace(/^components-/, '')}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
scan({
|
||||
enabled: false,
|
||||
showToolbar: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getAlgorithm = (themes: ThemeName[] = []) =>
|
||||
@@ -69,6 +56,23 @@ const getAlgorithm = (themes: ThemeName[] = []) =>
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// 获取最终主题(优先级:URL Query > Local Storage > Site (Memory))
|
||||
const getFinalTheme = (urlTheme: ThemeName[]): ThemeName[] => {
|
||||
// 只认 light/dark
|
||||
const baseTheme = urlTheme.filter((t) => !['light', 'dark', 'auto'].includes(t));
|
||||
const urlColor = urlTheme.find((t) => t === 'light' || t === 'dark');
|
||||
if (urlColor) return [...baseTheme, urlColor];
|
||||
|
||||
if (isLocalStorageNameSupported()) {
|
||||
const stored = localStorage.getItem(ANT_DESIGN_SITE_THEME) as ThemeName;
|
||||
if (stored && ['light', 'dark', 'auto'].includes(stored)) {
|
||||
return [...baseTheme, stored];
|
||||
}
|
||||
}
|
||||
// 默认 auto
|
||||
return [...baseTheme, 'auto'];
|
||||
};
|
||||
|
||||
const GlobalLayout: React.FC = () => {
|
||||
const outlet = useOutlet();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -80,8 +84,6 @@ const GlobalLayout: React.FC = () => {
|
||||
bannerVisible: false,
|
||||
});
|
||||
|
||||
const [systemTheme, setSystemTheme] = React.useState<'dark' | 'light'>(() => getSystemTheme());
|
||||
|
||||
// TODO: This can be remove in v6
|
||||
const useCssVar = searchParams.get('cssVar') !== 'false';
|
||||
|
||||
@@ -89,10 +91,9 @@ const GlobalLayout: React.FC = () => {
|
||||
(props: SiteState) => {
|
||||
setSiteState((prev) => ({ ...prev, ...props }));
|
||||
|
||||
// updating `searchParams` will clear the hash
|
||||
const oldSearchStr = searchParams.toString();
|
||||
|
||||
let nextSearchParams: URLSearchParams = searchParams;
|
||||
|
||||
(Object.entries(props) as Entries<SiteContextProps>).forEach(([key, value]) => {
|
||||
if (key === 'direction') {
|
||||
if (value === 'rtl') {
|
||||
@@ -102,14 +103,18 @@ const GlobalLayout: React.FC = () => {
|
||||
}
|
||||
}
|
||||
if (key === 'theme') {
|
||||
nextSearchParams = createSearchParams({
|
||||
...nextSearchParams,
|
||||
theme: value,
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector('html')
|
||||
?.setAttribute('data-prefers-color', value.includes('dark') ? 'dark' : 'light');
|
||||
const arr = Array.isArray(value) ? value : [value];
|
||||
const base = arr.filter((t) => !['light', 'dark', 'auto'].includes(t));
|
||||
const color = arr.find((t) => t === 'light' || t === 'dark');
|
||||
if (color) {
|
||||
nextSearchParams = createSearchParams({ ...nextSearchParams, theme: [...base, color] });
|
||||
} else {
|
||||
nextSearchParams.delete('theme');
|
||||
}
|
||||
// 设置 data-prefers-color
|
||||
if (color) {
|
||||
document.querySelector('html')?.setAttribute('data-prefers-color', color);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -117,13 +122,15 @@ const GlobalLayout: React.FC = () => {
|
||||
setSearchParams(nextSearchParams);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[searchParams, setSearchParams],
|
||||
);
|
||||
|
||||
const updateMobileMode = () => {
|
||||
const updateMobileMode = useCallback(() => {
|
||||
updateSiteConfig({ isMobile: window.innerWidth < RESPONSIVE_MOBILE });
|
||||
};
|
||||
}, [updateSiteConfig]);
|
||||
|
||||
// 监听系统主题变化
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -131,48 +138,32 @@ const GlobalLayout: React.FC = () => {
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
|
||||
const newSystemTheme = e.matches ? 'dark' : 'light';
|
||||
setSystemTheme(newSystemTheme);
|
||||
|
||||
const urlTheme = searchParams.getAll('theme') as ThemeName[];
|
||||
const hasUserColorTheme = urlTheme.includes('dark') || urlTheme.includes('light');
|
||||
if (!hasUserColorTheme) {
|
||||
setSiteState((prev) => ({
|
||||
...prev,
|
||||
theme: [...urlTheme.filter((t) => t !== 'dark' && t !== 'light'), newSystemTheme],
|
||||
}));
|
||||
|
||||
document.documentElement.setAttribute(
|
||||
'data-prefers-color',
|
||||
newSystemTheme === 'dark' ? 'dark' : 'light',
|
||||
);
|
||||
}
|
||||
};
|
||||
const handleSystemThemeChange = () => {};
|
||||
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
||||
};
|
||||
}, [searchParams, setSiteState]);
|
||||
}, []);
|
||||
|
||||
// 主题初始化
|
||||
useEffect(() => {
|
||||
const _theme = searchParams.getAll('theme') as ThemeName[];
|
||||
const hasUserColorTheme = _theme.includes('dark') || _theme.includes('light');
|
||||
const finalTheme = hasUserColorTheme
|
||||
? _theme
|
||||
: [..._theme.filter((t) => t !== 'dark' && t !== 'light'), systemTheme];
|
||||
const urlTheme = searchParams.getAll('theme') as ThemeName[];
|
||||
const finalTheme = getFinalTheme(urlTheme);
|
||||
const _direction = searchParams.get('direction') as DirectionType;
|
||||
|
||||
setSiteState({
|
||||
theme: finalTheme,
|
||||
direction: _direction === 'rtl' ? 'rtl' : 'ltr',
|
||||
});
|
||||
document.documentElement.setAttribute(
|
||||
'data-prefers-color',
|
||||
finalTheme.includes('dark') ? 'dark' : 'light',
|
||||
);
|
||||
|
||||
// 设置 data-prefers-color 属性
|
||||
const colorTheme = finalTheme.find((t) => ['light', 'dark'].includes(t));
|
||||
if (colorTheme) {
|
||||
document.documentElement.setAttribute('data-prefers-color', colorTheme);
|
||||
}
|
||||
|
||||
// Handle isMobile
|
||||
updateMobileMode();
|
||||
|
||||
@@ -186,7 +177,8 @@ const GlobalLayout: React.FC = () => {
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateMobileMode);
|
||||
};
|
||||
}, [systemTheme]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, updateMobileMode]);
|
||||
|
||||
const siteContextValue = React.useMemo<SiteContextProps>(
|
||||
() => ({
|
||||
@@ -199,17 +191,18 @@ const GlobalLayout: React.FC = () => {
|
||||
[isMobile, direction, updateSiteConfig, theme, bannerVisible],
|
||||
);
|
||||
|
||||
const themeConfig = React.useMemo<ThemeConfig>(
|
||||
() => ({
|
||||
algorithm: getAlgorithm(theme),
|
||||
const themeConfig = React.useMemo<ThemeConfig>(() => {
|
||||
// 算法优先级:auto 时用系统主题算法
|
||||
const themeForAlgorithm = theme;
|
||||
return {
|
||||
algorithm: getAlgorithm(themeForAlgorithm),
|
||||
token: { motion: !theme.includes('motion-off') },
|
||||
cssVar: useCssVar,
|
||||
hashed: !useCssVar,
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
};
|
||||
}, [theme, useCssVar]);
|
||||
|
||||
const [styleCache] = React.useState(() => createCache());
|
||||
const styleCache = React.useMemo(() => createCache(), []);
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
const styleText = extractStyle(styleCache, {
|
||||
|
||||
@@ -116,7 +116,7 @@ const AffixTabs: React.FC = () => {
|
||||
return () => {
|
||||
listenerEvents.forEach((event) => window.removeEventListener(event, onSyncAffix));
|
||||
};
|
||||
}, []);
|
||||
}, [onSyncAffix]);
|
||||
|
||||
return (
|
||||
<div className={classNames(affixTabs, fixedId && affixTabsFixed)} ref={containerRef}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createStyles } from 'antd-style';
|
||||
import CommonHelmet from '../../common/CommonHelmet';
|
||||
import Content from '../../slots/Content';
|
||||
import Sidebar from '../../slots/Sidebar';
|
||||
import { useSearchParams } from 'dumi';
|
||||
|
||||
const useStyle = createStyles(({ css, token }) => ({
|
||||
main: css`
|
||||
@@ -14,11 +15,14 @@ const useStyle = createStyles(({ css, token }) => ({
|
||||
}));
|
||||
|
||||
const SidebarLayout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const hideLayout = searchParams.get('layout') === 'false';
|
||||
|
||||
const { styles } = useStyle();
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<CommonHelmet />
|
||||
<Sidebar />
|
||||
{!hideLayout && <Sidebar />}
|
||||
<Content>{children}</Content>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"app.theme.switch.dynamic": "Dynamic Theme",
|
||||
"app.theme.switch.default": "Default theme",
|
||||
"app.theme.switch.auto": "Follow System",
|
||||
"app.theme.switch.light": "Light theme",
|
||||
"app.theme.switch.dark": "Dark theme",
|
||||
"app.theme.switch.compact": "Compact theme",
|
||||
"app.theme.switch.motion.on": "Motion On",
|
||||
@@ -112,9 +113,11 @@
|
||||
"app.footer.xtech.slogan": "Experience The Beauty",
|
||||
"app.footer.galacean": "Galacean",
|
||||
"app.footer.galacean.slogan": "Interactive Graphics Solution",
|
||||
"app.footer.weavefox": "WeaveFox",
|
||||
"app.footer.weavefox.slogan": "AI Development with WeaveFox 🦊",
|
||||
"app.docs.color.pick-primary": "Pick your primary color",
|
||||
"app.docs.color.pick-background": "Pick your background color",
|
||||
"app.docs.components.icon.search.placeholder": "Search icons here, click icon to copy code",
|
||||
"app.docs.components.icon.search.placeholder": "Search {total} icons here, click icon to copy code",
|
||||
"app.docs.components.icon.outlined": "Outlined",
|
||||
"app.docs.components.icon.filled": "Filled",
|
||||
"app.docs.components.icon.two-tone": "Two Tone",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"app.theme.switch.dynamic": "动态主题",
|
||||
"app.theme.switch.default": "默认主题",
|
||||
"app.theme.switch.auto": "跟随系统",
|
||||
"app.theme.switch.light": "浅色主题",
|
||||
"app.theme.switch.dark": "暗黑主题",
|
||||
"app.theme.switch.compact": "紧凑主题",
|
||||
"app.theme.switch.motion.on": "动画开启",
|
||||
@@ -111,9 +112,11 @@
|
||||
"app.footer.xtech.slogan": "让用户体验美好",
|
||||
"app.footer.galacean": "Galacean",
|
||||
"app.footer.galacean.slogan": "互动图形解决方案",
|
||||
"app.footer.weavefox": "WeaveFox",
|
||||
"app.footer.weavefox.slogan": "前端智能研发",
|
||||
"app.docs.color.pick-primary": "选择你的主色",
|
||||
"app.docs.color.pick-background": "选择你的背景色",
|
||||
"app.docs.components.icon.search.placeholder": "在此搜索图标,点击图标可复制代码",
|
||||
"app.docs.components.icon.search.placeholder": "在此搜索 {total} 个图标,点击图标可复制代码",
|
||||
"app.docs.components.icon.outlined": "线框风格",
|
||||
"app.docs.components.icon.filled": "实底风格",
|
||||
"app.docs.components.icon.two-tone": "双色风格",
|
||||
|
||||
@@ -165,6 +165,13 @@ const RoutesPlugin = async (api: IApi) => {
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../CHANGELOG.zh-CN.md'),
|
||||
},
|
||||
{
|
||||
id: 'components-changelog-cn',
|
||||
path: 'components/changelog-cn',
|
||||
absPath: '/changelog-cn',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../CHANGELOG.zh-CN.md'),
|
||||
},
|
||||
{
|
||||
id: 'changelog',
|
||||
path: 'changelog',
|
||||
@@ -172,6 +179,13 @@ const RoutesPlugin = async (api: IApi) => {
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../CHANGELOG.en-US.md'),
|
||||
},
|
||||
{
|
||||
id: 'components-changelog',
|
||||
path: 'components/changelog',
|
||||
absPath: '/changelog',
|
||||
parentId: 'DocLayout',
|
||||
file: resolve('../../CHANGELOG.en-US.md'),
|
||||
},
|
||||
];
|
||||
|
||||
extraRoutesList.forEach((itemRoute) => {
|
||||
|
||||
@@ -8,10 +8,8 @@ interface ContributorAvatarProps {
|
||||
}
|
||||
|
||||
const ContributorAvatar: React.FC<ContributorAvatarProps> = (props) => {
|
||||
const {
|
||||
item: { username, url } = {},
|
||||
} = props;
|
||||
if (username?.includes('github-actions')) {
|
||||
const { item: { username, url } = {} } = props;
|
||||
if (!username) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user