Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
branches: [main]
workflow_dispatch:
workflow_run:
workflows: ['Publish npm package']
types: [completed]

permissions:
contents: read
Expand All @@ -14,6 +17,7 @@ concurrency:

jobs:
build:
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
137 changes: 137 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Publish npm package

on:
push:
tags: ['v*']
workflow_dispatch:

permissions:
contents: read

concurrency:
group: npm-release
cancel-in-progress: false

jobs:
publish:
if: github.repository == 'ifhover/leaf-ui'
runs-on: ubuntu-latest
timeout-minutes: 30
environment: npm
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '26'
registry-url: 'https://registry.npmjs.org'
- name: Validate release tag, source commit and npm version
id: release
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
node --input-type=module <<'NODE'
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { appendFileSync, readFileSync } from 'node:fs';

const { RELEASE_TAG: tag, GITHUB_REF: ref, GITHUB_SHA: runCommit } = process.env;
assert.match(tag, /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/, 'Use a vX.Y.Z stable release tag, including when running the workflow manually.');
assert.equal(ref, `refs/tags/${tag}`, 'Select the release tag under Run workflow, not a branch.');
const git = (...args) => execFileSync('git', args, { encoding: 'utf8' }).trim();
const commit = git('rev-parse', 'HEAD');
assert.equal(commit, runCommit, 'The checkout must match the workflow run commit for provenance.');
assert.equal(git('rev-parse', `refs/tags/${tag}^{commit}`), commit, 'The checkout must match the release tag.');
git('merge-base', '--is-ancestor', commit, 'origin/main');
const pkg = JSON.parse(readFileSync('packages/react/package.json', 'utf8'));
assert.equal(pkg.name, '@ifhover/leaf-ui');
assert.equal(tag, `v${pkg.version}`, 'The tag must match packages/react/package.json.');
assert.equal(pkg.repository.url, 'git+https://github.com/ifhover/leaf-ui.git');

const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`, {
signal: AbortSignal.timeout(30_000),
});
assert.ok(response.ok || response.status === 404, `npm registry returned ${response.status}.`);
const registry = response.status === 404 ? {} : await response.json();
const published = registry.versions?.[pkg.version];
const latest = registry['dist-tags']?.latest;
if (published) {
assert.equal(published.gitHead, commit, 'This version was already published from a different commit. Bump the version.');
assert.equal(latest, pkg.version, 'This version is no longer latest; refusing to republish an older release.');
console.log(`${pkg.name}@${pkg.version} is already published from this commit; skipping the upload.`);
} else if (latest) {
assert.match(latest, /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/, 'npm latest must be a stable version.');
const currentParts = pkg.version.split('.').map(BigInt);
const latestParts = latest.split('.').map(BigInt);
const differing = currentParts.findIndex((part, index) => part !== latestParts[index]);
assert.ok(differing >= 0 && currentParts[differing] > latestParts[differing], 'The release version must be newer than npm latest.');
}
appendFileSync(process.env.GITHUB_OUTPUT, `version=${pkg.version}\ncommit=${commit}\nalready_published=${Boolean(published)}\n`);
NODE
- uses: pnpm/action-setup@v4
if: steps.release.outputs.already_published != 'true'
- name: Install dependencies
if: steps.release.outputs.already_published != 'true'
run: pnpm install --frozen-lockfile
- name: Check source, tests and release notes
if: steps.release.outputs.already_published != 'true'
run: pnpm check
- name: Build and verify library and documentation
if: steps.release.outputs.already_published != 'true'
run: pnpm build
- name: Ensure the build did not change release sources
if: steps.release.outputs.already_published != 'true'
run: |
if [[ -n "$(git status --porcelain)" ]]; then
git status --short
echo 'Commit generated source changes before publishing.' >&2
exit 1
fi
- name: Install npm with trusted publishing support
if: steps.release.outputs.already_published != 'true'
run: npm install --global npm@11
- name: Publish to npm using OIDC
if: steps.release.outputs.already_published != 'true'
working-directory: packages/react
run: npm publish --access public --tag latest --provenance --registry https://registry.npmjs.org/
- name: Verify published commit and latest tag
env:
RELEASE_VERSION: ${{ steps.release.outputs.version }}
RELEASE_COMMIT: ${{ steps.release.outputs.commit }}
run: |
node --input-type=module <<'NODE'
import assert from 'node:assert/strict';
import { appendFileSync } from 'node:fs';
import { setTimeout } from 'node:timers/promises';

const { RELEASE_VERSION: version, RELEASE_COMMIT: commit } = process.env;
for (let attempt = 1; attempt <= 12; attempt++) {
try {
const response = await fetch('https://registry.npmjs.org/@ifhover%2Fleaf-ui', {
cache: 'no-store',
signal: AbortSignal.timeout(30_000),
});
assert.ok(response.ok, `npm registry returned ${response.status}.`);
const registry = await response.json();
assert.equal(registry.versions?.[version]?.gitHead, commit, 'npm gitHead does not match the release commit.');
assert.equal(registry['dist-tags']?.latest, version, 'npm latest does not match the release version.');
appendFileSync(process.env.GITHUB_STEP_SUMMARY, [
`Verified npm package: [@ifhover/leaf-ui@${version}](https://www.npmjs.com/package/@ifhover/leaf-ui/v/${version})`,
`Release tag: v${version}; commit: ${commit}`,
'The Deploy documentation workflow runs after this workflow succeeds. Check its result before considering the release complete.',
'',
].join('\n\n'));
break;
} catch (error) {
if (attempt === 12) throw error;
console.log(`Waiting for npm registry metadata (${attempt}/12): ${error.message}`);
await setTimeout(10_000);
}
}
NODE
2 changes: 2 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
with:
name: production-bundles
path: packages/react/.quality
include-hidden-files: true

react-compatibility:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -71,6 +72,7 @@ jobs:
if: always()
with:
name: browser-${{ matrix.browser }}
include-hidden-files: true
path: |
packages/react/playwright-report
packages/react/test-results
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
apps/docs/docs/public/locales/
dist/
doc_build/
site_build/
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

开始开发、修改使用文档或准备发布前,必须阅读并遵守 [开发、版本与文档同步流程](DEVELOPMENT-WORKFLOW.md)。该文档是分支开发、正式发布、开发文档预览与清理的统一约定。

新增、修改或重构组件前,必须阅读并遵守 [组件编写与开发规则](packages/react/DEVELOPMENT.md) 和 [样式与交互标准](DESIGN-STANDARDS.md)。组件的公共 API、区域默认参数、内部控件、复合组件、表单语义和验证要求均以这些规则为准。

## 核心规则

- `main` 是稳定发布分支。组件代码和使用文档的改动必须在新分支完成,合并到 `main` 意味着完成一次新版本发布。
Expand All @@ -16,8 +18,8 @@
## 相关说明

- 项目结构与本地开发:[DEVELOPMENT.md](DEVELOPMENT.md)
- 组件实现约定:[packages/react/DEVELOPMENT.md](packages/react/DEVELOPMENT.md)
- 组件编写与开发规则:[packages/react/DEVELOPMENT.md](packages/react/DEVELOPMENT.md)
- 样式与交互标准:[DESIGN-STANDARDS.md](DESIGN-STANDARDS.md)
- 发布操作:[packages/react/RELEASING.md](packages/react/RELEASING.md)
- 文档构建与快照:[apps/docs/README.md](apps/docs/README.md)
- 文档构建与快照:[apps/docs/AGENTS.md](apps/docs/AGENTS.md)
- 质量验收:[packages/react/QUALITY.md](packages/react/QUALITY.md)
2 changes: 1 addition & 1 deletion DESIGN-REFRESH.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ All 83 catalog entries were reviewed. The seven static or direct manipulation pr
| Button, Input, InputNumber, Textarea, Checkbox, Radio, Switch, Slider, Rate, InputOTP, InputMask, SignaturePad, ImageCropper, ColorPicker | Neutral field hierarchy, focus/hover/press feedback, continuous check/thumb changes, measured textarea growth, stable loading button width/name, validation and tool feedback. Typing, drawing, cropping and pointer dragging remain immediate. |
| Select, AutoComplete, Mentions, Cascader, TreeSelect, Transfer | Refined trigger and option surfaces, opening origins, chevrons, selection feedback, column/list changes; virtual data windows do not animate as ordinary rows. |
| Calendar, DatePicker, DateTimePicker, DateRangePicker, TimePicker, TimeRangePicker | Directional view changes, shared calendar surfaces, direct date range selection, simultaneous time endpoints, continuous time selection and keyboard feedback. |
| Tabs, Segmented, Menu, Steps, Breadcrumb, Pagination, Anchor, BackTop, FloatButton, AppBar | Measured shared selection indicators, retained tab panels, directional content and height transitions, progress rings, navigation hover/press, staggered quick actions. Geometry handles scrolling, RTL, resized text and scaled ancestors. |
| Tabs, Segmented, Menu, Steps, Breadcrumb, Pagination, Anchor, BackTop, FloatButton, AppBar | Measured shared selection indicators, immediately switched tab panels with retained state, progress rings, navigation hover/press, staggered quick actions. Geometry handles scrolling, RTL, resized text and scaled ancestors. |
| Collapse, Tree, OrgChart, Carousel | Natural expansion/collapse with retained inert exit content, branch chevrons, interrupted list layout motion, zoom-aware geometry, directional overlapping carousel slides and natural host heights. Virtual Tree remains immediate. |
| Modal, Drawer, Confirm, Popover, Popconfirm, Tooltip, Dropdown, CommandPalette, Tour | Coordinated masks/surfaces, actual floating origins, moving highlights, natural content height changes, preserved inputs/focus, orderly exit presence, async generation isolation and correct Tour step references. |
| Alert, Message, Notification, Loading, LoadingBar, Progress, Result, ErrorBoundary, Skeleton, Statistic | State/icon feedback, continuous queue collapse, progress changes and completion, resolved skeleton content reveal without an extra wrapper, immediate numeric values with subtle update motion. Empty uses the Result family. |
Expand Down
Loading
Loading