@jc-times/business-ui 0.2.55 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,55 +1,55 @@
1
- # @jc-times/business-ui
2
-
3
- 精诚时代业务应用共用的 React 基础组件与交互标准。公共包负责通用 UI、无障碍与响应式;权限、接口、业务文案和合同、客户、订单、ERP 等领域状态由消费应用提供。
4
-
5
- **Agent 从 [docs/agent-guide.md](docs/agent-guide.md) 开始阅读:按需求选组件,再查 API 和专题。** 本仓库 [AGENTS.md](AGENTS.md) 同样指向该入口。
6
-
7
- ## 安装与引入
8
-
9
- 内部 registry 为 GitHub Packages(`https://npm.pkg.github.com`)。按 [发布与消费说明](docs/release.md) 配置访问,选择已发布的精确版本并提交锁文件;受控 tarball 消费也必须固定版本和制品。不要复制源码、跨仓库引用源文件或使用目录软链接。
10
-
11
- ```tsx
12
- import { Button, ModalShell } from "@jc-times/business-ui";
13
- import "@jc-times/business-ui/styles.css";
14
- ```
15
-
16
- React 等 peer dependencies 按所安装版本的 package.json 配置。应用通过 `--ui-*` 语义令牌提供主题;普通控件默认 40px,粗指针触屏最小 44px。
17
-
18
- 文档查看器、行政区划数据、纯金额格式化使用独立子入口;对应导入与依赖见专题文档。仓库工作树中的新入口不代表消费方已安装或该能力已发布。
19
-
20
- ## 文档导航
21
-
22
- | 文档 | 用途 |
23
- | --- | --- |
1
+ # @jc-times/business-ui
2
+
3
+ 精诚时代业务应用共用的 React 基础组件与交互标准。公共包负责通用 UI、无障碍与响应式;权限、接口、业务文案和合同、客户、订单、ERP 等领域状态由消费应用提供。
4
+
5
+ **Agent 从 [docs/agent-guide.md](docs/agent-guide.md) 开始阅读:按需求选组件,再查 API 和专题。** 本仓库 [AGENTS.md](AGENTS.md) 同样指向该入口。
6
+
7
+ ## 安装与引入
8
+
9
+ 内部 registry 为 GitHub Packages(`https://npm.pkg.github.com`)。按 [发布与消费说明](docs/release.md) 配置访问,选择已发布的精确版本并提交锁文件;受控 tarball 消费也必须固定版本和制品。不要复制源码、跨仓库引用源文件或使用目录软链接。
10
+
11
+ ```tsx
12
+ import { Button, ModalShell } from "@jc-times/business-ui";
13
+ import "@jc-times/business-ui/styles.css";
14
+ ```
15
+
16
+ React 等 peer dependencies 按所安装版本的 package.json 配置。应用通过 `--ui-*` 语义令牌提供主题;普通控件默认 40px,粗指针触屏最小 44px。
17
+
18
+ 文档查看器、行政区划数据、纯金额格式化使用独立子入口;对应导入与依赖见专题文档。仓库工作树中的新入口不代表消费方已安装或该能力已发布。
19
+
20
+ ## 文档导航
21
+
22
+ | 文档 | 用途 |
23
+ | --- | --- |
24
24
  | [Agent 使用指南](docs/agent-guide.md) | 场景选型、组件组合、边界和接入检查 |
25
25
  | [依赖维护](docs/dependencies.md) | 稳定版本核查、兼容例外、消费迁移要求 |
26
26
  | [公共 API](docs/api.md) | 公共行为、参数说明、数字输入与装饰示例;完整签名见安装包类型声明 |
27
27
  | [DataTable 与 MRT 迁移](docs/data-table.md) | 公共表格替代、旧参数兼容、完整配置、独立入口与合并明细;0.2.31起提供 |
28
28
  | [表格能力与 MRT 样例](docs/mrt-poc.md) | DataTable边界、筛选/列管理、产品行合并/展开、配置及性能;包含业务样例与公共API边界 |
29
- | [分段控制器](docs/api.md#分段控制器-segmentedcontrol) | 少量互斥选项、浅底/图标/纵向示例、兼容边界 |
30
- | [组件设计策略](docs/component-strategy.md) | 设计依据、公共能力范围和交互内核策略 |
31
- | [弹窗迁移](docs/modal-shell-migration.md) | header/content/footer 稳定插槽与历史写法迁移 |
32
- | [成熟度迁移](docs/maturity-migration.md) | 日期、多选、目录缓存、表格、文件限制及 Toast 行为 |
33
- | [上传前处理](docs/file-preparation.md) | 文件异步前处理、整批提交与取消 |
34
- | [文档查看器](docs/document-viewer.md) | PDF.js 与图片查看入口 |
29
+ | [分段控制器](docs/api.md#分段控制器-segmentedcontrol) | 少量互斥选项、浅底/图标/纵向示例、兼容边界 |
30
+ | [组件设计策略](docs/component-strategy.md) | 设计依据、公共能力范围和交互内核策略 |
31
+ | [弹窗迁移](docs/modal-shell-migration.md) | header/content/footer 稳定插槽与历史写法迁移 |
32
+ | [成熟度迁移](docs/maturity-migration.md) | 日期、多选、目录缓存、表格、文件限制及 Toast 行为 |
33
+ | [上传前处理](docs/file-preparation.md) | 文件异步前处理、整批提交与取消 |
34
+ | [文档查看器](docs/document-viewer.md) | PDF.js 与图片查看入口 |
35
35
  | [文档与图片查看器 V2](docs/document-viewer-v2.md) | PDFium/WASM、原生页面叠层、图片及 V1 兼容入口与迁移边界 |
36
- | [多视口验收](docs/viewport-qa.md) | 桌面、移动与交互验证 |
37
- | [发布流程](docs/release.md) | registry、版本、制品与发布检查 |
38
- | [变更记录](CHANGELOG.md) | 按版本查看变化 |
39
-
40
- 详细用法集中于 API 和专题;新增公共能力时同步 Agent 选型表和发布文档清单。历史审计文档保留作证据,不作为当前 API 说明。
41
-
42
- ## 开发与消费边界
43
-
44
- - 通用缺陷、无障碍、响应式及跨项目能力在公共包修复;单项目领域需求通过业务包装实现。
45
- - 优先兼容既有 API;破坏性修改需要 major 版本及迁移说明。
46
- - 消费方使用公开插槽和 props,固定弹窗底栏使用 footer;不要依赖内部 DOM 或重复实现焦点管理。
47
- - 已配置 `business-ui-gate` 的消费项目继续运行门禁;首次接入需要生成并审核基线,不通过扩大 baseline 掩盖新增手写组件。
48
-
49
- ## 本地验证与发布
50
-
51
- `pnpm dev:matrix` 打开组件验收页面;`pnpm verify` 执行类型、单元测试、库和矩阵构建;`pnpm test:browser` 验证真实浏览器交互。详细要求见多视口验收与发布流程。
52
-
53
- 仓库通过 `v<package.version>` 标签触发受控发布。公共包本地验证、正式发布、消费应用升级和生产部署是不同状态,应分别验证和记录。
54
-
36
+ | [多视口验收](docs/viewport-qa.md) | 桌面、移动与交互验证 |
37
+ | [发布流程](docs/release.md) | registry、版本、制品与发布检查 |
38
+ | [变更记录](CHANGELOG.md) | 按版本查看变化 |
39
+
40
+ 详细用法集中于 API 和专题;新增公共能力时同步 Agent 选型表和发布文档清单。历史审计文档保留作证据,不作为当前 API 说明。
41
+
42
+ ## 开发与消费边界
43
+
44
+ - 通用缺陷、无障碍、响应式及跨项目能力在公共包修复;单项目领域需求通过业务包装实现。
45
+ - 优先兼容既有 API;破坏性修改需要 major 版本及迁移说明。
46
+ - 消费方使用公开插槽和 props,固定弹窗底栏使用 footer;不要依赖内部 DOM 或重复实现焦点管理。
47
+ - 已配置 `business-ui-gate` 的消费项目继续运行门禁;首次接入需要生成并审核基线,不通过扩大 baseline 掩盖新增手写组件。
48
+
49
+ ## 本地验证与发布
50
+
51
+ `pnpm dev:matrix` 打开组件验收页面;`pnpm verify` 执行类型、单元测试、库和矩阵构建;`pnpm test:browser` 验证真实浏览器交互。详细要求见多视口验收与发布流程。
52
+
53
+ 仓库通过 `v<package.version>` 标签触发受控发布。公共包本地验证、正式发布、消费应用升级和生产部署是不同状态,应分别验证和记录。
54
+
55
55
  - [通用级联选择与部门/地址适配](docs/cascader.md)
@@ -1,7 +1,7 @@
1
- # Third-party notices
2
-
3
- `@jc-times/business-ui` 使用以下固定版本开源依赖。依赖源码未复制到本仓库;最终安装包由包管理器连同各自许可证一并解析。
4
-
1
+ # Third-party notices
2
+
3
+ `@jc-times/business-ui` 使用以下固定版本开源依赖。依赖源码未复制到本仓库;最终安装包由包管理器连同各自许可证一并解析。
4
+
5
5
  | Package | Version | License | Project |
6
6
  | --- | --- | --- | --- |
7
7
  | `lucide-react` | 1.45.0(开发验证版本) | ISC | https://github.com/lucide-icons/lucide |
@@ -12,20 +12,20 @@
12
12
  | `@emotion/react`, `@emotion/styled` | 11.14.0 / 11.14.1 | MIT | https://github.com/emotion-js/emotion |
13
13
  | `@embedpdf/react-pdf-viewer` | 2.15.0 | MIT | https://github.com/embedpdf/embed-pdf-viewer |
14
14
  | `@embedpdf/core`, `@embedpdf/engines`, `@embedpdf/plugin-document-manager`, `@embedpdf/plugin-viewport`, `@embedpdf/plugin-scroll`, `@embedpdf/plugin-render`, `@embedpdf/plugin-zoom`, `@embedpdf/plugin-rotate`, `@embedpdf/plugin-thumbnail` | 2.15.0 | MIT | https://github.com/embedpdf/embed-pdf-viewer |
15
- | PDFium(由 `@embedpdf/pdfium` 随查看器分发) | 2.15.0 | BSD-3-Clause / Apache-2.0 等,详见依赖内 `LICENSE.pdfium` | https://pdfium.googlesource.com/pdfium/ |
16
- | `@radix-ui/react-popover` | 1.1.23 | MIT | https://github.com/radix-ui/primitives |
17
- | `@radix-ui/react-dialog` | 1.1.23 | MIT | https://github.com/radix-ui/primitives |
18
- | `@radix-ui/react-toast` | 1.2.23 | MIT | https://github.com/radix-ui/primitives |
19
- | `@internationalized/date` | 3.12.4 | Apache-2.0 | https://github.com/adobe/react-spectrum |
20
- | `@radix-ui/react-select` | 2.3.7 | MIT | https://github.com/radix-ui/primitives |
21
- | `@radix-ui/react-tooltip` | 1.2.16 | MIT | https://github.com/radix-ui/primitives |
22
- | `react-aria-components` | 1.21.1 | Apache-2.0 | https://github.com/adobe/react-spectrum |
23
- | `cn-division` | 2026.0.1 | MIT | https://github.com/kk-418/cn-division |
24
-
25
- 完整许可证文本以各依赖包内的 `LICENSE` 文件为准。
26
-
27
- ## @rc-component/cascader 1.25.0
28
-
15
+ | PDFium(由 `@embedpdf/pdfium` 随查看器分发) | 2.15.0 | BSD-3-Clause / Apache-2.0 等,详见依赖内 `LICENSE.pdfium` | https://pdfium.googlesource.com/pdfium/ |
16
+ | `@radix-ui/react-popover` | 1.1.23 | MIT | https://github.com/radix-ui/primitives |
17
+ | `@radix-ui/react-dialog` | 1.1.23 | MIT | https://github.com/radix-ui/primitives |
18
+ | `@radix-ui/react-toast` | 1.2.23 | MIT | https://github.com/radix-ui/primitives |
19
+ | `@internationalized/date` | 3.12.4 | Apache-2.0 | https://github.com/adobe/react-spectrum |
20
+ | `@radix-ui/react-select` | 2.3.7 | MIT | https://github.com/radix-ui/primitives |
21
+ | `@radix-ui/react-tooltip` | 1.2.16 | MIT | https://github.com/radix-ui/primitives |
22
+ | `react-aria-components` | 1.21.1 | Apache-2.0 | https://github.com/adobe/react-spectrum |
23
+ | `cn-division` | 2026.0.1 | MIT | https://github.com/kk-418/cn-division |
24
+
25
+ 完整许可证文本以各依赖包内的 `LICENSE` 文件为准。
26
+
27
+ ## @rc-component/cascader 1.25.0
28
+
29
29
  MIT, Copyright (c) 2019-present react-component. Source: https://github.com/react-component/cascader. Used as the external Cascader interaction dependency; license reproduced in licenses/rc-cascader-MIT.txt. Transitive dependencies retain their own notices in their installed packages.
30
30
 
31
31
  0.2.46 additionally bundles the React gesture entry of @embedpdf/plugin-zoom@2.15.0 with a pinned bounds/precision patch. Original MIT license: licenses/embedpdf-MIT.txt; patch: patches/@embedpdf__plugin-zoom@2.15.0.patch. Engine and core plugin implementations remain external.
@@ -1,171 +1,171 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import ts from "typescript";
5
-
6
- const args = process.argv.slice(2);
7
- const root = process.cwd();
8
- const sourceRoot = path.resolve(root, value("--root") ?? "src");
9
- const baselinePath = path.resolve(root, value("--baseline") ?? "scripts/business-ui-gate-baseline.json");
10
- const only = new Set((value("--only") ?? "").split(",").filter(Boolean));
11
- const snapshot = filterSnapshot(scan(sourceRoot));
12
-
13
- if (args.includes("--snapshot")) {
14
- process.stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
15
- process.exit(0);
16
- }
17
- if (!fs.existsSync(baselinePath)) {
18
- process.stderr.write(`缺少门禁基线:${baselinePath}\n请先使用 --snapshot 生成并审核基线。\n`);
19
- process.exit(1);
20
- }
21
-
22
- const baseline = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
23
- const regressions = [];
24
- for (const [file, current] of Object.entries(snapshot)) {
25
- for (const [kind, count] of Object.entries(current)) {
26
- const allowed = baseline[file]?.[kind] ?? 0;
27
- if (count > allowed) regressions.push({ file, kind, count, allowed });
28
- }
29
- }
30
- if (regressions.length) {
31
- process.stderr.write(`发现新增的未统一 UI 实现:\n${regressions.map(formatRegression).join("\n")}\n`);
32
- process.exit(1);
33
- }
34
- process.stdout.write("Business UI gate passed.\n");
35
-
36
- function value(flag) {
37
- const index = args.indexOf(flag);
38
- return index < 0 ? undefined : args[index + 1];
39
- }
40
-
41
- function filterSnapshot(input) {
42
- if (!only.size) return input;
43
- const output = {};
44
- for (const [file, counts] of Object.entries(input)) {
45
- const selected = Object.fromEntries(Object.entries(counts).filter(([kind]) => only.has(kind)));
46
- if (Object.keys(selected).length) output[file] = selected;
47
- }
48
- return output;
49
- }
50
-
51
- function formatRegression({ file, kind, count, allowed }) {
52
- const base = `- ${file}: ${kind} ${count}(基线 ${allowed})`;
53
- if (kind !== "legacyModalFooter") return base;
54
- return `${base}\n ModalShell 的固定底栏必须改用 footer 属性,并通过 footerClassName 定制;不要把原生 <footer> 直接放在 children 中。`;
55
- }
56
-
57
- function scan(directory) {
58
- const result = {};
59
- for (const absolute of walk(directory)) {
60
- if (!absolute.endsWith(".tsx") || absolute.endsWith(".test.tsx")) continue;
61
- const source = fs.readFileSync(absolute, "utf8");
62
- const sourceFile = ts.createSourceFile(absolute, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
63
- const modalImports = collectModalImports(sourceFile);
64
- const counts = {
65
- bareButton: 0,
66
- bareInput: 0,
67
- bareSelect: 0,
68
- bareTextarea: 0,
69
- handwrittenDialog: 0,
70
- handwrittenMenu: 0,
71
- handwrittenCombobox: 0,
72
- legacyModalFooter: 0,
73
- };
74
- visit(sourceFile);
75
- const used = Object.fromEntries(Object.entries(counts).filter(([, count]) => count));
76
- if (Object.keys(used).length) result[path.relative(root, absolute).replaceAll(path.sep, "/")] = used;
77
-
78
- function visit(node) {
79
- if (ts.isJsxElement(node) && isImportedModalTag(node.openingElement.tagName, modalImports)
80
- && !hasAttribute(node.openingElement, "footer") && node.children.some(rendersDirectFooter)) {
81
- counts.legacyModalFooter += 1;
82
- }
83
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
84
- const tag = node.tagName.getText(sourceFile);
85
- const hasClass = hasAttribute(node, "className");
86
- if (tag === "button" && !hasClass) counts.bareButton += 1;
87
- if (tag === "input" && !exemptInput(node)) counts.bareInput += 1;
88
- if (tag === "select") counts.bareSelect += 1;
89
- if (tag === "textarea") counts.bareTextarea += 1;
90
- const role = stringAttribute(node, "role");
91
- if (/^[a-z]/.test(tag) && (role === "dialog" || role === "alertdialog")) counts.handwrittenDialog += 1;
92
- if (/^[a-z]/.test(tag) && (role === "menu" || role === "menuitem")) counts.handwrittenMenu += 1;
93
- if (/^[a-z]/.test(tag) && (role === "combobox" || role === "listbox" || role === "option")) counts.handwrittenCombobox += 1;
94
- }
95
- ts.forEachChild(node, visit);
96
- }
97
-
98
- function exemptInput(node) {
99
- const type = stringAttribute(node, "type");
100
- return ["file", "hidden", "range"].includes(type ?? "");
101
- }
102
-
103
- function stringAttribute(node, name) {
104
- const attribute = node.attributes.properties.find((item) => ts.isJsxAttribute(item) && item.name.getText(sourceFile) === name);
105
- return attribute && ts.isJsxAttribute(attribute) && attribute.initializer && ts.isStringLiteral(attribute.initializer)
106
- ? attribute.initializer.text : undefined;
107
- }
108
-
109
- function hasAttribute(node, name) {
110
- return node.attributes.properties.some((item) => ts.isJsxAttribute(item) && item.name.getText(sourceFile) === name);
111
- }
112
-
113
- function rendersDirectFooter(node) {
114
- if (ts.isJsxElement(node)) return node.openingElement.tagName.getText(sourceFile) === "footer";
115
- if (ts.isJsxSelfClosingElement(node)) return node.tagName.getText(sourceFile) === "footer";
116
- if (ts.isJsxFragment(node)) return node.children.some(rendersDirectFooter);
117
- if (ts.isJsxExpression(node) && node.expression) return expressionRendersFooter(node.expression);
118
- return false;
119
- }
120
-
121
- function expressionRendersFooter(expression) {
122
- if (ts.isParenthesizedExpression(expression)) return expressionRendersFooter(expression.expression);
123
- if (ts.isJsxElement(expression) || ts.isJsxSelfClosingElement(expression) || ts.isJsxFragment(expression)) {
124
- return rendersDirectFooter(expression);
125
- }
126
- if (ts.isConditionalExpression(expression)) {
127
- return expressionRendersFooter(expression.whenTrue) || expressionRendersFooter(expression.whenFalse);
128
- }
129
- if (ts.isBinaryExpression(expression) && [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken,
130
- ts.SyntaxKind.QuestionQuestionToken].includes(expression.operatorToken.kind)) {
131
- return expressionRendersFooter(expression.left) || expressionRendersFooter(expression.right);
132
- }
133
- if (ts.isArrayLiteralExpression(expression)) return expression.elements.some(expressionRendersFooter);
134
- return false;
135
- }
136
- }
137
- return Object.fromEntries(Object.entries(result).sort(([a], [b]) => a.localeCompare(b)));
138
- }
139
-
140
- function collectModalImports(sourceFile) {
141
- const names = new Set();
142
- const namespaces = new Set();
143
- for (const statement of sourceFile.statements) {
144
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)
145
- || statement.moduleSpecifier.text !== "@jc-times/business-ui") continue;
146
- const bindings = statement.importClause?.namedBindings;
147
- if (bindings && ts.isNamedImports(bindings)) {
148
- for (const element of bindings.elements) {
149
- const importedName = (element.propertyName ?? element.name).text;
150
- if (importedName === "ModalShell" || importedName === "Dialog") names.add(element.name.text);
151
- }
152
- } else if (bindings && ts.isNamespaceImport(bindings)) {
153
- namespaces.add(bindings.name.text);
154
- }
155
- }
156
- return { names, namespaces };
157
- }
158
-
159
- function isImportedModalTag(tagName, { names, namespaces }) {
160
- if (ts.isIdentifier(tagName)) return names.has(tagName.text);
161
- if (!ts.isPropertyAccessExpression(tagName) || !ts.isIdentifier(tagName.expression)) return false;
162
- return namespaces.has(tagName.expression.text) && (tagName.name.text === "ModalShell" || tagName.name.text === "Dialog");
163
- }
164
-
165
- function* walk(directory) {
166
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
167
- const absolute = path.join(directory, entry.name);
168
- if (entry.isDirectory()) yield* walk(absolute);
169
- else yield absolute;
170
- }
171
- }
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import ts from "typescript";
5
+
6
+ const args = process.argv.slice(2);
7
+ const root = process.cwd();
8
+ const sourceRoot = path.resolve(root, value("--root") ?? "src");
9
+ const baselinePath = path.resolve(root, value("--baseline") ?? "scripts/business-ui-gate-baseline.json");
10
+ const only = new Set((value("--only") ?? "").split(",").filter(Boolean));
11
+ const snapshot = filterSnapshot(scan(sourceRoot));
12
+
13
+ if (args.includes("--snapshot")) {
14
+ process.stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
15
+ process.exit(0);
16
+ }
17
+ if (!fs.existsSync(baselinePath)) {
18
+ process.stderr.write(`缺少门禁基线:${baselinePath}\n请先使用 --snapshot 生成并审核基线。\n`);
19
+ process.exit(1);
20
+ }
21
+
22
+ const baseline = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
23
+ const regressions = [];
24
+ for (const [file, current] of Object.entries(snapshot)) {
25
+ for (const [kind, count] of Object.entries(current)) {
26
+ const allowed = baseline[file]?.[kind] ?? 0;
27
+ if (count > allowed) regressions.push({ file, kind, count, allowed });
28
+ }
29
+ }
30
+ if (regressions.length) {
31
+ process.stderr.write(`发现新增的未统一 UI 实现:\n${regressions.map(formatRegression).join("\n")}\n`);
32
+ process.exit(1);
33
+ }
34
+ process.stdout.write("Business UI gate passed.\n");
35
+
36
+ function value(flag) {
37
+ const index = args.indexOf(flag);
38
+ return index < 0 ? undefined : args[index + 1];
39
+ }
40
+
41
+ function filterSnapshot(input) {
42
+ if (!only.size) return input;
43
+ const output = {};
44
+ for (const [file, counts] of Object.entries(input)) {
45
+ const selected = Object.fromEntries(Object.entries(counts).filter(([kind]) => only.has(kind)));
46
+ if (Object.keys(selected).length) output[file] = selected;
47
+ }
48
+ return output;
49
+ }
50
+
51
+ function formatRegression({ file, kind, count, allowed }) {
52
+ const base = `- ${file}: ${kind} ${count}(基线 ${allowed})`;
53
+ if (kind !== "legacyModalFooter") return base;
54
+ return `${base}\n ModalShell 的固定底栏必须改用 footer 属性,并通过 footerClassName 定制;不要把原生 <footer> 直接放在 children 中。`;
55
+ }
56
+
57
+ function scan(directory) {
58
+ const result = {};
59
+ for (const absolute of walk(directory)) {
60
+ if (!absolute.endsWith(".tsx") || absolute.endsWith(".test.tsx")) continue;
61
+ const source = fs.readFileSync(absolute, "utf8");
62
+ const sourceFile = ts.createSourceFile(absolute, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
63
+ const modalImports = collectModalImports(sourceFile);
64
+ const counts = {
65
+ bareButton: 0,
66
+ bareInput: 0,
67
+ bareSelect: 0,
68
+ bareTextarea: 0,
69
+ handwrittenDialog: 0,
70
+ handwrittenMenu: 0,
71
+ handwrittenCombobox: 0,
72
+ legacyModalFooter: 0,
73
+ };
74
+ visit(sourceFile);
75
+ const used = Object.fromEntries(Object.entries(counts).filter(([, count]) => count));
76
+ if (Object.keys(used).length) result[path.relative(root, absolute).replaceAll(path.sep, "/")] = used;
77
+
78
+ function visit(node) {
79
+ if (ts.isJsxElement(node) && isImportedModalTag(node.openingElement.tagName, modalImports)
80
+ && !hasAttribute(node.openingElement, "footer") && node.children.some(rendersDirectFooter)) {
81
+ counts.legacyModalFooter += 1;
82
+ }
83
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
84
+ const tag = node.tagName.getText(sourceFile);
85
+ const hasClass = hasAttribute(node, "className");
86
+ if (tag === "button" && !hasClass) counts.bareButton += 1;
87
+ if (tag === "input" && !exemptInput(node)) counts.bareInput += 1;
88
+ if (tag === "select") counts.bareSelect += 1;
89
+ if (tag === "textarea") counts.bareTextarea += 1;
90
+ const role = stringAttribute(node, "role");
91
+ if (/^[a-z]/.test(tag) && (role === "dialog" || role === "alertdialog")) counts.handwrittenDialog += 1;
92
+ if (/^[a-z]/.test(tag) && (role === "menu" || role === "menuitem")) counts.handwrittenMenu += 1;
93
+ if (/^[a-z]/.test(tag) && (role === "combobox" || role === "listbox" || role === "option")) counts.handwrittenCombobox += 1;
94
+ }
95
+ ts.forEachChild(node, visit);
96
+ }
97
+
98
+ function exemptInput(node) {
99
+ const type = stringAttribute(node, "type");
100
+ return ["file", "hidden", "range"].includes(type ?? "");
101
+ }
102
+
103
+ function stringAttribute(node, name) {
104
+ const attribute = node.attributes.properties.find((item) => ts.isJsxAttribute(item) && item.name.getText(sourceFile) === name);
105
+ return attribute && ts.isJsxAttribute(attribute) && attribute.initializer && ts.isStringLiteral(attribute.initializer)
106
+ ? attribute.initializer.text : undefined;
107
+ }
108
+
109
+ function hasAttribute(node, name) {
110
+ return node.attributes.properties.some((item) => ts.isJsxAttribute(item) && item.name.getText(sourceFile) === name);
111
+ }
112
+
113
+ function rendersDirectFooter(node) {
114
+ if (ts.isJsxElement(node)) return node.openingElement.tagName.getText(sourceFile) === "footer";
115
+ if (ts.isJsxSelfClosingElement(node)) return node.tagName.getText(sourceFile) === "footer";
116
+ if (ts.isJsxFragment(node)) return node.children.some(rendersDirectFooter);
117
+ if (ts.isJsxExpression(node) && node.expression) return expressionRendersFooter(node.expression);
118
+ return false;
119
+ }
120
+
121
+ function expressionRendersFooter(expression) {
122
+ if (ts.isParenthesizedExpression(expression)) return expressionRendersFooter(expression.expression);
123
+ if (ts.isJsxElement(expression) || ts.isJsxSelfClosingElement(expression) || ts.isJsxFragment(expression)) {
124
+ return rendersDirectFooter(expression);
125
+ }
126
+ if (ts.isConditionalExpression(expression)) {
127
+ return expressionRendersFooter(expression.whenTrue) || expressionRendersFooter(expression.whenFalse);
128
+ }
129
+ if (ts.isBinaryExpression(expression) && [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken,
130
+ ts.SyntaxKind.QuestionQuestionToken].includes(expression.operatorToken.kind)) {
131
+ return expressionRendersFooter(expression.left) || expressionRendersFooter(expression.right);
132
+ }
133
+ if (ts.isArrayLiteralExpression(expression)) return expression.elements.some(expressionRendersFooter);
134
+ return false;
135
+ }
136
+ }
137
+ return Object.fromEntries(Object.entries(result).sort(([a], [b]) => a.localeCompare(b)));
138
+ }
139
+
140
+ function collectModalImports(sourceFile) {
141
+ const names = new Set();
142
+ const namespaces = new Set();
143
+ for (const statement of sourceFile.statements) {
144
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)
145
+ || statement.moduleSpecifier.text !== "@jc-times/business-ui") continue;
146
+ const bindings = statement.importClause?.namedBindings;
147
+ if (bindings && ts.isNamedImports(bindings)) {
148
+ for (const element of bindings.elements) {
149
+ const importedName = (element.propertyName ?? element.name).text;
150
+ if (importedName === "ModalShell" || importedName === "Dialog") names.add(element.name.text);
151
+ }
152
+ } else if (bindings && ts.isNamespaceImport(bindings)) {
153
+ namespaces.add(bindings.name.text);
154
+ }
155
+ }
156
+ return { names, namespaces };
157
+ }
158
+
159
+ function isImportedModalTag(tagName, { names, namespaces }) {
160
+ if (ts.isIdentifier(tagName)) return names.has(tagName.text);
161
+ if (!ts.isPropertyAccessExpression(tagName) || !ts.isIdentifier(tagName.expression)) return false;
162
+ return namespaces.has(tagName.expression.text) && (tagName.name.text === "ModalShell" || tagName.name.text === "Dialog");
163
+ }
164
+
165
+ function* walk(directory) {
166
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
167
+ const absolute = path.join(directory, entry.name);
168
+ if (entry.isDirectory()) yield* walk(absolute);
169
+ else yield absolute;
170
+ }
171
+ }
@@ -0,0 +1,4 @@
1
+ import { Button as e, LinkButton as t, buttonClassName as n } from "./Button.js";
2
+ import { Alert as r } from "./Alert.js";
3
+ import { Checkbox as i, FormField as a, SelectInput as o, TextArea as s, TextInput as c } from "./FormField.js";
4
+ export { r as Alert, e as Button, i as Checkbox, a as FormField, t as LinkButton, o as SelectInput, s as TextArea, c as TextInput, n as buttonClassName };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Lightweight entry point for simple authenticated forms and standalone pages.
3
+ * It deliberately excludes directory pickers, date controls, data tables and
4
+ * document viewers so consumers do not pull their dependency graphs.
5
+ */
6
+ export { Alert, type AlertProps, type AlertTone } from "./Alert";
7
+ export { Button, LinkButton, buttonClassName, type ButtonProps, type ButtonVariant, type ButtonSize, type LinkButtonProps } from "./Button";
8
+ export { Checkbox, FormField, SelectInput, TextArea, TextInput, type TextInputProps, type TextAreaProps, type SelectInputProps } from "./FormField";
@@ -5,7 +5,7 @@ import { useCapability as c, useDocumentState as l, usePlugin as u } from "@embe
5
5
  import { useViewportElement as d } from "@embedpdf/plugin-viewport/react";
6
6
  import { ZoomPlugin as f, initialDocumentState as p } from "@embedpdf/plugin-zoom";
7
7
  export * from "@embedpdf/plugin-zoom";
8
- //#region node_modules/.pnpm/@embedpdf+plugin-zoom@2.15._aede46f2a75cbc54dc0fefd84f941023/node_modules/@embedpdf/plugin-zoom/dist/react/index.js
8
+ //#region node_modules/.pnpm/@embedpdf+plugin-zoom@2.15.0_patch_hash=21cc50a50730a2920846ef4e8bb72d24d69849e9fd43d6f_aede46f2a75cbc54dc0fefd84f941023/node_modules/@embedpdf/plugin-zoom/dist/react/index.js
9
9
  var m = /* @__PURE__ */ e({
10
10
  MarqueeZoom: () => w,
11
11
  ZoomGestureWrapper: () => C,