Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .cursor/skills/nutui-build-local-verify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: nutui-build-local-verify
description: NutUI 比例缩放本地验证——写回 src/packages 下同路径组件 SCSS(跳过 src/packages/**/demo.scss 与 demos);--mirror 写 scale-verify/;不写 build。
disable-model-invocation: true
---

# NutUI Build Local Verify

## 在做什么

**只做一步**:用 `scripts/px-to-scale-px-in-component-scss.cjs` 把组件 SCSS 里裸 `px` 转成 `scale-px` 等,并把结果写回磁盘。

**不扫描、不写入**:**`src/packages/<组件名>/demo.scss`**(各组件目录根下的单文件)、`**/demos/**`、路径中含 **`/demo/`**、测试与快照目录下的 `.scss`(与官方 `build.mjs` 里对 `**/demo.scss` 的 ignore 一致)。

- **默认(就地覆盖)**:对每个匹配的 `.scss`,**读、写都是同一路径**——相对 `src/packages` 的路径不变。例如 `src/packages/actionsheet/actionsheet.scss` 转换后仍写回该文件,不会改到别的目录或改名。
- **`--mirror`**:不写源码;结果写到 **`scale-verify/<与 src/packages 相同的相对路径>`**(例如 `scale-verify/actionsheet/actionsheet.scss`),便于 diff。

之后是否再跑 `pnpm run build`、是否用别的工具核对,由你自行决定;本 skill **不要求** build。

## 覆盖原 SCSS(推荐)

在 **nutui-react 仓库根目录** 执行。**务必先 commit / stash**,用完 `git restore src/packages` 或 `git checkout -- src/packages` 恢复。

若只需还原 **`src/packages/<组件>/demo.scss`**(当前脚本已跳过;若曾被旧版本误改):

```bash
find src/packages -name 'demo.scss' -exec git restore -- {} \;
```

**然后**在仓库根执行验证:

```bash
pnpm run verify-scale
```

等价:

```bash
node .cursor/skills/nutui-build-local-verify/scripts/verify-scale-generation.mjs
```

(`--in-place` / `-i` 与默认等价。)

## 报告

路径:**`scale-verify/report.json`**。覆盖模式下看 `overwriteSource === true`、`changedFileCount`、`changedFiles`。

## 其它命令

```bash
# 删除仓库根下 scale-verify/ 整目录(含 report;不还原已覆盖的 src/packages)
node .cursor/skills/nutui-build-local-verify/scripts/verify-scale-generation.mjs --clean
```

**可选**(只镜像、不覆盖源码):

```bash
pnpm run verify-scale:mirror
```

`--mirror` 与 `--in-place` 不能同时使用。

## 核对清单

- [ ] 覆盖前已 git 可回滚
- [ ] `changedFiles` 抽样无 `scale-px(0px)`、无重复嵌套 `scale-px`
- [ ] `font-size` / `font` 未被误改(转换器会跳过)

## 给用户的一句话结论

- 脚本跑完 + `changedFileCount` + 列 2~3 个 `changedFiles`
- **覆盖的是真实源码**时,验证完用 **git 恢复**
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env node
/**
* 本地验证:默认就地写回 src/packages 下同一路径的组件 .scss(如 …/actionsheet/actionsheet.scss)。
* 跳过 src/packages/**/demo.scss、demos、测试与快照(与 build.mjs ignore 一致)。
* --mirror 只写 scale-verify/;不包含 build;自行 git diff / 恢复即可。
*/
import fs from 'node:fs/promises'
import path from 'path'
import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)
const transform = require(path.resolve(process.cwd(), 'scripts/px-to-scale-px-in-component-scss.cjs'))

const repoRoot = process.cwd()
const packagesRoot = path.resolve(repoRoot, 'src/packages')
const outRoot = path.resolve(repoRoot, 'scale-verify')
const reportPath = path.resolve(outRoot, 'report.json')

const argv = new Set(process.argv.slice(2))
const shouldClean = argv.has('--clean')
const mirrorMode = argv.has('--mirror')
/** 默认覆盖 src/packages 原 .scss;传 --mirror 则只写 scale-verify/ */
const inPlace = !mirrorMode

if (mirrorMode && (argv.has('--in-place') || argv.has('-i'))) {
console.error('[scale-verify] 不能同时使用 --mirror 与 --in-place / -i')
process.exit(1)
}

function isScssFile(name) {
return name.endsWith('.scss')
}

function shouldSkip(relPath) {
const p = relPath.replaceAll('\\', '/')
// 与 build.mjs 的 ignore 一致:**/demo.scss 不参与 px→scale 写回
if (path.posix.basename(p) === 'demo.scss') return true
if (p.includes('/demo/')) return true
if (p.includes('/demos/')) return true
if (p.includes('/__test__/')) return true
if (p.includes('/__tests__/')) return true
if (p.includes('/__snapshots__/')) return true
if (p.startsWith('.scale-verify/')) return true
return false
}

async function walkScssFiles(dir, base = dir, list = []) {
const entries = await fs.readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const abs = path.resolve(dir, entry.name)
const rel = path.relative(base, abs)
if (entry.isDirectory()) {
await walkScssFiles(abs, base, list)
continue
}
if (!entry.isFile() || !isScssFile(entry.name)) continue
if (shouldSkip(rel)) continue
list.push(abs)
}
return list
}

async function ensureReportDir() {
await fs.mkdir(outRoot, { recursive: true })
}

async function prepareOutputLayout() {
if (shouldClean) {
await fs.rm(outRoot, { recursive: true, force: true })
console.log('[scale-verify] cleaned:', path.relative(repoRoot, outRoot))
return
}

await fs.rm(outRoot, { recursive: true, force: true })
await fs.mkdir(outRoot, { recursive: true })
}

async function main() {
await prepareOutputLayout()
if (shouldClean) {
return
}

const files = await walkScssFiles(packagesRoot)
files.sort()

const changed = []
for (const absFile of files) {
const rel = path.relative(packagesRoot, absFile)
const source = await fs.readFile(absFile, 'utf8')
const transformed = transform(source)
if (source === transformed) continue

const targetFile = inPlace ? absFile : path.resolve(outRoot, rel)
if (!inPlace) {
await fs.mkdir(path.dirname(targetFile), { recursive: true })
}
await fs.writeFile(targetFile, transformed, 'utf8')
changed.push(rel.replaceAll('\\', '/'))
}

await ensureReportDir()
const scssWriteRoot = inPlace
? path.relative(repoRoot, packagesRoot).replaceAll('\\', '/')
: path.relative(repoRoot, outRoot).replaceAll('\\', '/')

const report = {
generatedAt: new Date().toISOString(),
mode: inPlace ? 'in-place' : 'mirror',
overwriteSource: inPlace,
/** 本次写入的 SCSS 根路径:原地为 src/packages,镜像为仓库根下 scale-verify */
scssWriteRoot,
/** 镜像模式下的实验目录;原地模式为 null */
outDir: inPlace ? null : path.relative(repoRoot, outRoot).replaceAll('\\', '/'),
reportPath: path.relative(repoRoot, reportPath).replaceAll('\\', '/'),
totalScssFiles: files.length,
changedFileCount: changed.length,
changedFiles: changed,
}
await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8')

console.log('[scale-verify] mode:', report.mode)
if (!inPlace) {
console.log('[scale-verify] outDir:', report.outDir)
} else {
console.log('[scale-verify] wrote into:', path.relative(repoRoot, packagesRoot))
}
console.log('[scale-verify] totalScssFiles:', report.totalScssFiles)
console.log('[scale-verify] changedFileCount:', report.changedFileCount)
console.log('[scale-verify] report:', path.relative(repoRoot, reportPath))
}

main().catch((err) => {
console.error('[scale-verify] failed:', err)
process.exitCode = 1
})
118 changes: 118 additions & 0 deletions .cursor/skills/nutui-proportional-scaling/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
name: nutui-proportional-scaling
description: >-
NutUI React proportional scaling on branch feat_resize: runtime --nut-scale-f /
--nut-scale-font / --nut-scale-icon from scale-f.ts (H5) and scale-f.taro.ts
(Taro), Sass helpers scale-px / scale-font-px / scale-icon-px and theme font
tokens in variables.scss & theme-*.scss; npm run build / build:taro run
scripts/px-to-scale-px-in-component-scss.cjs on component SCSS in memory; profiles standard / large / elderly;
commit-backed rules e.g. never scale 0px. Use when implementing 多尺寸适配,
等比适配, 大字版, 老年版, scale-px, viewport or native bridge scaling, or
editing component SCSS for resize; SCSS: prefer calc($token + Npx) over
#{} in calc, use outer calc() when mixing tokens that compile to
var(--nutui-*).
---

# NutUI React 等比适配

## 1. 运行时:谁在写 CSS 变量

- **H5**:`src/utils/scale-f.ts`
- `initScaleF(profile?)`:首次计算缩放、`resize` 时 `refreshScaleF`。
- `getScaleF`:优先 `jmfe.callNative('DongScreenAdapterPlugin','getScale')`,失败用视口规则。
- 视口回退要点:`innerWidth >= 600` 视作 pad,基准乘 `1.2`;`375–600` 间按 `375` 比例,**上限 1.17**(与源码常量一致)。
- **Taro 侧复用同一套契约**:`src/utils/scale-f.taro.ts`,并从 `src/utils/index.taro.ts` 导出。
- 写入 `:root` 的变量(与 `variables.scss` 一致):
- `--nut-scale-f`:布局/通用 `scale-px`
- `--nut-scale-font`:`scale-font-px`、主题 `--nutui-font-size-*`
- `--nut-scale-icon`:`scale-icon-px`、图标相关

**档位 `ScaleProfile`**:`standard` | `large` | `elderly`(仅后两者生效额外倍率)。
**场景倍率**(与 `getSceneRatio` 一致):老年对 `font` / `icon` / `lego` × `1.3`;大字仅对 `font` × `1.15`。

**JS 里算像素**:`calcByProfile(baseValue, { scene, profile?, scale?, device? })` — 用于组件内联样式、画布尺寸等,与 Sass 的 `calc(...* var(--nut-scale-*))` 同一套语义。

---

## 2. 样式层:`variables.scss` 中的函数

```scss
// 根上默认值见 variables.scss :root
@function scale-px($size) {
@return calc(#{$size} * var(--nut-scale-f, 1));
}
@function scale-font-px($size) {
@return calc(#{$size} * var(--nut-scale-font, var(--nut-scale-f, 1)));
}
@function scale-icon-px($size) {
@return calc(#{$size} * var(--nut-scale-icon, var(--nut-scale-f, 1)));
}
```

**主题字号档**(`theme-default.scss` / `theme-dark.scss`):`--nutui-font-size-*` 使用 `calc(Npx * var(--nut-scale-font, var(--nut-scale-f, 1)))`,与 **大字/老年** 档位对齐。

### 2.1 `npm run build` / `npm run build:taro` 时的 px → `scale-px`

- 与 `package.json` 中顺序一致:先跑 **`scripts/replace-css-var.js`**,再 **`scripts/build.mjs`** 或 **`scripts/build-taro.mjs`**;上述脚本在读取 **`src/packages/**/\*.scss`(不含 demo)** 后,会经 **`scripts/px-to-scale-px-in-component-scss.cjs`** 在**内存**里把声明值中的裸 **`Npx`** 转为 **`scale-px(Npx)`**(规则见 §3),**不写回\*\*仓库里的组件 SCSS。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

修复 Markdown 格式问题。

此行存在 Markdown 转义错误,不写回\*\*仓库里的组件 中的 \*\* 会显示为字面星号而非加粗结束标记,导致渲染异常。

📝 建议的修复
-在**内存**里把声明值中的裸 **`Npx`** 转为 **`scale-px(Npx)`**(规则见 §3),**不写回\*\*仓库里的组件 SCSS。
+在**内存**里把声明值中的裸 **`Npx`** 转为 **`scale-px(Npx)`**(规则见 §3),**不写回**仓库里的组件 SCSS。
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-`package.json` 中顺序一致:先跑 **`scripts/replace-css-var.js`**,再 **`scripts/build.mjs`****`scripts/build-taro.mjs`**;上述脚本在读取 **`src/packages/**/\*.scss`(不含 demo)** 后,会经 **`scripts/px-to-scale-px-in-component-scss.cjs`****内存**里把声明值中的裸 **`Npx`** 转为 **`scale-px(Npx)`**(规则见 §3),**不写回\*\*仓库里的组件 SCSS。
-`package.json` 中顺序一致:先跑 **`scripts/replace-css-var.js`**,再 **`scripts/build.mjs`****`scripts/build-taro.mjs`**;上述脚本在读取 **`src/packages/**/\*.scss`(不含 demo)** 后,会经 **`scripts/px-to-scale-px-in-component-scss.cjs`****内存**里把声明值中的裸 **`Npx`** 转为 **`scale-px(Npx)`**(规则见 §3),**不写回**仓库里的组件 SCSS。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/skills/nutui-proportional-scaling/SKILL.md at line 56, The Markdown
contains an escaped bold marker (\*\*) causing literal asterisks to render;
update the sentence that mentions scripts (scripts/replace-css-var.js,
scripts/build.mjs, scripts/build-taro.mjs,
scripts/px-to-scale-px-in-component-scss.cjs) and change the fragment
"不写回\*\*仓库里的组件 SCSS" to use a proper bold marker (e.g., "不写回**仓库里的组件 SCSS**" or
remove the asterisks entirely) so the bold formatting closes correctly and no
backslashes remain.

- 源码里可继续手写 **`scale-px` / `scale-font-px` / `scale-icon-px`**;构建不会重复嵌套 `scale-px`。
- 该脚本对 **`calc(...)` 体内同时含 `$` 与 `/`** 的整段先做占位再替换裸 `px`,避免 postcss-scss 把 **`calc($var / 2)`** 等拆坏;其它 `calc` 内的裸 `Npx` 仍会按规则转为 `scale-px`。

---

## 3. 提交里固化的规范(务必遵守)

### 3.1 「0px 不转换」(`1a35d9b8`)

凡应为 **数值 0** 的尺寸,**不要**写 `scale-px(0px)`,一律 **`0`** 或 **`0px`**(如 padding 某一维、`box-shadow` 偏移、border 为 0、`margin: 0`)。
否则会得到 `calc(0px * var(--nut-scale-f))`,在部分浏览器或亚像素场景下与纯 `0` 表现不一致。

### 3.2 `line-height`

- **比例行高**(如 `line-height: 1`):不随系数变,用于挤压行盒、图标对齐等 — 与「等比 px」不同维度。
- **与设计稿 px 绑定的行高**:用与字号一致的档位,通常为 **`scale-font-px`**,或与同一变量体系。
- 参考历史修复:弹层标题等曾去掉不恰当的固定 `line-height` 以免与大字模式冲突 — 新增时不要给标题随意写死 `line-height: 20px` 类样式,除非走缩放函数或主题变量。

### 3.3 组件 SCSS 迁移模式(`dc4f1e28` / 后续 style 提交)

- 间距、圆角、`border` 粗细、固定宽高(非纯文字):优先 **`scale-px`**。
- 纯字体大小:`scale-font-px` 或主题已有 `--nutui-font-size-*`。
- 图标占位:**`scale-icon-px`** 或已有 `--nut-icon-*`。
- 保持与 **无障碍/大屏** 相关提交协同:同一文件改尺度时,勿回退 `dialog` 等对大字兼容的改动。

### 3.5 组件 `.tsx` 图标尺寸治理(props → class → 变量)

- 对 `@nutui/icons-react` / `@nutui/icons-react-taro`:尽量避免在组件上写死 `size={12}`、`width={16}`、`height={16}`。
- **推荐模式**:在 `.tsx` 里只加语义化 `className`,到对应 `.scss` 里用变量控制尺寸(优先 `$icon-size-*` 阶梯,或组件专用变量)。
- 若是内联 `<svg>`(非 NutUI 图标组件)也遵循同一规则:移除 `width/height` 字面量,改为 class,并在 SCSS 用变量(可新增如 `$xxx-icon-size`,默认 `scale-icon-px(Npx)`)。
- 新增尺寸档优先沉淀到 `variables.scss`(如 `$icon-size-11`、`$icon-size-16`),避免同一像素值在多个组件重复散落。

### 3.4 `calc()`、Sass 变量与 `#{}`(与 `variables.scss` / 主题 token 一致)

- **推荐**:在 `calc()` 里直接写 Sass 变量,如 **`calc($steps-vertical-head-icon-size + 1px)`**、**`calc($rate-item-margin / 2)`**,而不是 **`calc(#{$steps-vertical-head-icon-size} + 1px)`**。`#{}` 只在需要把值**硬插成无引号 CSS 片段**、或要避免 Sass 对单位做提前合并时再考虑;普通设计 token 用 **`$var` 作为 `calc` 的操作数**即可。
- **与 `var(--nutui-*)` 一起运算时**:许多 token 会展开为 **`var(--nutui-…, calc(Npx * var(--nut-scale-f, 1)))`**。此时**不要**指望纯 Sass 括号在声明值里做「减法 + 固定 px」,例如
**`margin: 0 ($switch-height - $switch-border-width + 3px) 0 7px`**
会在编译结果里拼成 **`var(...)var(...)`** 一类**缺少运算符**的非法片段。应写成 **`margin: 0 calc($switch-height - $switch-border-width + 3px) 0 7px`**,让整条长度在**一个** CSS `calc()` 里由浏览器解析。
- **`100%` 与长度相减**:用 **`calc(100% - Npx)`** 一层即可,避免出现 **`100% - calc(...)`** 这类单位不合法的组合(历史上有过 postcss / 手工替换导致的损坏,以当前组件 SCSS 为准)。

---

## 4. 业务接入清单

1. **入口调用**:在应用入口(仅浏览器环境)调用 `initScaleF(可选档位)`;档位可随业务切换并依赖内部 `setScaleProfile` 刷新变量。
2. **Taro**:使用 `scale-f.taro` 导出,保证 H5 与小程序 WebView 行为一致(仍以仓库实现为准)。
3. **覆写主题**:通过 `--nutui-*` 或 `--nut-scale-*` 覆盖时,保持 `calc` 与变量回退链完整。
4. **验收**:切换标准/大字/老年、收窄与放宽视口、(如有)站内容器走原生 `getScale`,检查布局与字号是否同比变化且无「0px 被 scale」问题。

---

## 5. Agent 自检(改完缩放权相关代码时)

- [ ] 新增 **0** 尺寸未误用 `scale-px(0px)`。
- [ ] 字体/图标是否应走 **`scale-font-px` / `scale-icon-px`** 而非误用 `scale-px`。
- [ ] 含 **`$token` 与裸 `px` 的混合运算**:若 token 会变成 **`var(--nutui-*)`**,是否已用 **`calc($a - $b + Npx)`**,而不是 **`($a - $b + Npx)`** 写在 `margin` / `width` 等非纯编译期长度位置。
- [ ] `calc()` 内对设计 token 是否优先 **`calc($var + 1px)`**,避免无必要的 **`#{}`**。
- [ ] 组件 `.tsx` 是否仍有写死图标尺寸(`size/width/height`);如有,是否已改为 **class + SCSS 变量**(内联 `svg` 同理)。
- [ ] TS 侧改 `scale-f*` 已同步考虑 **Taro** 文件。
- [ ] 修改 `formatScaleValue` / 断点时,已通读 **视口与平板常量** 是否仍与文档、设计一致。

若与上游分支分歧,以**当前分支 `feat_resize` 最新提交**及 `src/utils/scale-f*.ts`、`src/styles/variables.scss` 为准。
2 changes: 0 additions & 2 deletions NutUI-React_组件标准白皮书.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ NutUI 的演进伴随着移动端技术的演进,从移动端到跨端,从 V
定义组件 Props 前,**必须(MUST)**继承全局抽象类型 `BasicComponent`(或 `MiniProgramBasicProps` 等多端公共类型),确保组件基础能力的统一。

2. **强类型声明与规范**:

- **禁止 `any`**:Props 参数需使用显式联合类型,严禁使用 `any` 逃避类型检查。
- **事件命名**:对外事件统一以 `onXXX`(小驼峰)命名。
- **精简通信**:事件通信仅传递必要数据,避免携带冗余的组件实例或私有变量。
Expand Down Expand Up @@ -150,7 +149,6 @@ export const Button = React.forwardRef<HTMLButtonElement, Partial<ButtonProps>>(
### 5.4 组件防御与容灾红线

1. **防阻断式崩溃**:

- 针对入参异常、异步失败、逻辑边界等风险点,必须内置 `try-catch` 或提供兜底 UI。
- 组件内部错误不得外溢导致整个前端应用白屏死机。

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@
"e2e:run:taro": "start-server-and-test dev:taro:h5 http://localhost:10086 cypress:run:taro",
"e2e:open:taro": "start-server-and-test dev:taro:h5 http://localhost:10086 cypress:open:taro",
"update:taro:entry": "node ./scripts/harmony/update-taro-entry",
"upgradeTaro": "pnpm --dir ./packages/nutui-taro-demo upgradeTaro"
"upgradeTaro": "pnpm --dir ./packages/nutui-taro-demo upgradeTaro",
"verify-scale": "node .cursor/skills/nutui-build-local-verify/scripts/verify-scale-generation.mjs",
"verify-scale:mirror": "node .cursor/skills/nutui-build-local-verify/scripts/verify-scale-generation.mjs --mirror"
},
"lint-staged": {
"*.{scss,md}": "prettier --write",
Expand Down
12 changes: 12 additions & 0 deletions packages/nutui-taro-demo/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component } from 'react'
import { initScaleF } from '@/utils/scale-f'
import('@/sites/assets/styles/reset.scss')
import('@/packages/nutui.react.scss.taro')
import('@nutui/touch-emulator')
Expand All @@ -7,6 +8,17 @@ import './app.scss'

// console.log(NutUI)
class App extends Component {
private disposeScale?: () => void

componentDidMount() {
// 写入 :root 的 --nut-scale-f / --nut-scale-font / --nut-scale-icon,与组件内 scale-* 一致
this.disposeScale = initScaleF('elderly')
}

componentWillUnmount() {
this.disposeScale?.()
}

render() {
return this.props.children
}
Expand Down
Loading
Loading