@jayyuen66/dsh-plugin-shared 0.1.9 → 0.2.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 +66 -63
- package/README.zh-CN.md +93 -90
- package/config/tsconfig.client.base.json +1 -1
- package/dist/card-ui.js +105 -0
- package/dist/config/oxlint.base.js +1 -1
- package/dist/config/vitest.base.js +1 -1
- package/dist/types/build-artifacts.d.ts +11 -11
- package/dist/types/build-client.d.ts +20 -20
- package/dist/types/build-host.d.ts +4 -4
- package/dist/types/build-types.d.ts +4 -4
- package/dist/types/canonicalize-region-paths.d.ts +1 -1
- package/dist/types/card-apply.d.ts +9 -9
- package/dist/types/card-ui/field.d.ts +33 -0
- package/dist/types/card-ui/hint.d.ts +11 -0
- package/dist/types/card-ui/inputs.d.ts +33 -0
- package/dist/types/card-ui/layout.d.ts +19 -0
- package/dist/types/card-ui/save-bar.d.ts +16 -0
- package/dist/types/card-ui/select.d.ts +15 -0
- package/dist/types/card-ui/styles.d.ts +2 -0
- package/dist/types/card-ui.d.ts +10 -0
- package/dist/types/config/oxlint.base.d.ts +36 -36
- package/dist/types/config/vitest.base.d.ts +3 -3
- package/dist/types/errors.d.ts +6 -6
- package/dist/types/job-outcome.d.ts +12 -12
- package/dist/types/jsonl.d.ts +6 -6
- package/dist/types/lesson-bus.d.ts +2 -2
- package/dist/types/locale.d.ts +9 -9
- package/dist/types/project-key.d.ts +6 -5
- package/dist/types/record.d.ts +16 -16
- package/dist/types/text.d.ts +4 -4
- package/dist/types/tool-events.d.ts +20 -20
- package/package.json +19 -5
package/README.zh-CN.md
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-plugin-shared</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><em>dsh
|
|
7
|
+
<p align="center"><em>dsh 插件家族共用的库包:一份工具事件记录表、卡片状态骨架、一套统一设置卡 UI、一个项目键与一组 locale 支持——只被插件 import,不由用户启用。</em></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/@jayyuen66/dsh-plugin-shared"><img src="https://img.shields.io/npm/v/@jayyuen66/dsh-plugin-shared?style=flat-square&logo=npm&logoColor=white" alt="npm version" /></a>
|
|
11
|
-
<a href="https://www.npmjs.com/package/@jayyuen66/dsh-plugin-shared"><img src="https://img.shields.io/npm/
|
|
11
|
+
<a href="https://www.npmjs.com/package/@jayyuen66/dsh-plugin-shared"><img src="https://img.shields.io/npm/d18m/@jayyuen66/dsh-plugin-shared?style=flat-square" alt="npm downloads (total)" /></a>
|
|
12
12
|
<a href="https://github.com/JayYuen666/dsh-plugin-shared/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/JayYuen666/dsh-plugin-shared/ci.yml?branch=main&style=flat-square&label=ci" alt="ci" /></a>
|
|
13
13
|
<img src="https://img.shields.io/badge/license-MIT-4c1?style=flat-square" alt="MIT" />
|
|
14
14
|
<img src="https://img.shields.io/badge/node-%3E%3D22.12-4c1?style=flat-square&logo=nodedotjs&logoColor=white" alt="node" />
|
|
15
|
-
<img src="https://img.shields.io/badge/dsh-%5E0.2.1--rc.
|
|
15
|
+
<img src="https://img.shields.io/badge/dsh-%5E0.2.1--rc.3-658EFF?style=flat-square" alt="dsh" />
|
|
16
16
|
<a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/powered_by_dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white" alt="powered by dsh" /></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
@@ -27,16 +27,16 @@
|
|
|
27
27
|
<a href="./README.md"><img src="https://img.shields.io/badge/lang-English-658EFF?style=flat-square" alt="English" /></a>
|
|
28
28
|
</p>
|
|
29
29
|
|
|
30
|
-
**一个 [dsh](https://github.com/deepseek-ai/deepseek-harness)
|
|
30
|
+
**一个 [dsh](https://github.com/deepseek-ai/deepseek-harness) 库包:同一作者的插件家族共用的运行期模块与 lint/test 基线。它只被插件包 import,用户不会启用它。**
|
|
31
31
|
|
|
32
32
|
## <img src="https://api.iconify.design/lucide/file-text.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 目录
|
|
33
33
|
|
|
34
34
|
- [它做什么](#它做什么)
|
|
35
35
|
- [安装](#安装)
|
|
36
|
-
- [
|
|
36
|
+
- [确认它在正常工作](#确认它在正常工作)
|
|
37
37
|
- [它不提供什么](#它不提供什么)
|
|
38
38
|
- [模块清单](#模块清单)
|
|
39
|
-
- [config
|
|
39
|
+
- [config 导出](#config-导出)
|
|
40
40
|
- [已知边界](#已知边界)
|
|
41
41
|
- [数据与隐私](#数据与隐私)
|
|
42
42
|
- [版本与兼容](#版本与兼容)
|
|
@@ -46,19 +46,19 @@
|
|
|
46
46
|
|
|
47
47
|
## <img src="https://api.iconify.design/lucide/shield.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 它做什么
|
|
48
48
|
|
|
49
|
-
- **它是库,不是插件。** `package.json` 里没有 `dsh` 字段,包目录下也没有 `cordis.patch.yml
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
49
|
+
- **它是库,不是插件。** `package.json` 里没有 `dsh` 字段,包目录下也没有 `cordis.patch.yml`,所以位于 `node_modules` 的副本不会被宿主登记成插件。它没有设置卡,没有客户端部分,也没有自己的路由。
|
|
50
|
+
- **它收录的是各插件逐字重复的部分:**
|
|
51
|
+
- 工具事件记录表(`./lib/tool-events`):一次扫描把 `SessionEvent[]` 变成调用表与结果表,PTC 子调用一并合并;`editPathOf` 提取其中可见的编辑目标。
|
|
52
|
+
- 客户端卡片基础组件(`./lib/card-apply`、`./lib/card-diff`):跨 HMR 保证 apply 幂等的保护逻辑,以及把 `null`、`undefined` 与键缺失当成同一种值的表单差异比较。
|
|
53
|
+
- 运行期工具(`./lib/project-key`、`./lib/locale`、`./lib/lesson-bus`、`./lib/text`、`./lib/record`、`./lib/errors`、`./lib/jsonl`、`./lib/job-outcome`):各个插件原本都要重写一遍的小逻辑。
|
|
54
|
+
- 构建期与检查基线(`./lib/canonicalize-region-paths`、`./config/*`):产物路径规范化,加上给每个消费方的 oxlint、vitest 与 tsconfig 起点。
|
|
55
|
+
- **它只做通用处理,不做领域决策。** 哪些工具算编辑、路径怎么过滤、预算多大、检查何时该报错,都留在各插件。共享层对交给它的领域不做判断。
|
|
56
56
|
|
|
57
57
|
### 收录边界
|
|
58
58
|
|
|
59
|
-
-
|
|
59
|
+
- **逐字相同是收录标准。** 同一份函数体在两处以上出现才收录。看起来相似、但失败语义、缓存方式或覆盖方式不同的,留在原地。
|
|
60
60
|
- **领域判定留在外面。** 依赖某个插件自己的工具清单、策略或面向用户的措辞的谓词,不是样板,即使它出现了两次。
|
|
61
|
-
-
|
|
61
|
+
- **发布出去的接口就是承诺。** 新增模块要同时加子路径、barrel 再导出(构建期那一项除外)、构建脚本入口、构建 `include` 与用例;漏一处会被「开发」一节的一致性检查判定失败。
|
|
62
62
|
|
|
63
63
|
## <img src="https://api.iconify.design/lucide/package.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 安装
|
|
64
64
|
|
|
@@ -74,52 +74,52 @@ pnpm add @jayyuen66/dsh-plugin-shared
|
|
|
74
74
|
import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
终端用户不需要单独安装或启用它:任一消费包安装时都会自动包含它。它缺失时的失败是 `ERR_MODULE_NOT_FOUND`(说明符指向 `@jayyuen66/dsh-plugin-shared/...`),不会静默变成其他行为。本包没有 default 导出;请从 barrel 或子路径使用具名导入。
|
|
78
78
|
|
|
79
79
|
| 要求 | 值 |
|
|
80
80
|
| --- | --- |
|
|
81
|
-
| Node.js | `>=22.12.0
|
|
82
|
-
| dsh | `^0.2.1-rc.2`,声明为 **optional** peer(import
|
|
81
|
+
| Node.js | `>=22.12.0`——插件家族共用的发布侧下限,声明同一个 `engines.node` 的包不会因为这个库而被拒 |
|
|
82
|
+
| dsh | `^0.2.1-rc.2`,声明为 **optional** peer(import 本包不需要宿主存在) |
|
|
83
83
|
| 模块形态 | 仅 ESM。`require()` 只在支持 `require(esm)` 的 Node 上可用 |
|
|
84
84
|
| Tree shaking | `"sideEffects": false` |
|
|
85
85
|
|
|
86
|
-
###
|
|
86
|
+
### 依赖要求
|
|
87
87
|
|
|
88
|
-
`dependencies` 为空;所有 `@deepseek-ai/*` 只出现在 `peerDependencies` 与 `devDependencies` 里。`test/publish-manifest.test.ts`
|
|
88
|
+
`dependencies` 为空;所有 `@deepseek-ai/*` 只出现在 `peerDependencies` 与 `devDependencies` 里。`test/publish-manifest.test.ts` 固定了这条要求,理由是一次启动期故障,不是风格偏好:
|
|
89
89
|
|
|
90
|
-
- **宿主包写进 `dependencies`
|
|
90
|
+
- **宿主包写进 `dependencies` 会让消费方的宿主无法工作。** profile 用 `nodeLinker: hoisted` 与 `autoInstallPeers: false` 安装,于是列在 `dependencies` 里的宿主包会被安装成真实目录,与宿主进程已加载的那份并存。`@deepseek-ai/dsh-tools` 的工具运行期调度键是普通 `Symbol`(不是 `Symbol.for`),符号身份按模块实例计算:宿主从副本上读那个键得到 `undefined`,于是每一次原生工具调用都抛错。构建、类型检查与测试在此之前全部通过;这条不变式只能写在 manifest 上。
|
|
91
91
|
- **`@deepseek-ai/dsh-output-retention` 是唯一必需 peer。** 它是值导入(`truncateEnd`),运行期由宿主提供;在 `peerDependenciesMeta` 里明写成 `"optional": false`,不靠"没写就等于必需",因为没写进那张表对 npm 7+ 就是硬要求。
|
|
92
|
-
- **其余 peer 都是 optional。** 除裸 `@deepseek-ai/dsh
|
|
93
|
-
-
|
|
92
|
+
- **其余 peer 都是 optional。** 除裸 `@deepseek-ai/dsh`(只声明宿主版本线)外,它们只承载类型声明。放在 `peerDependencies` 里让发布出去的 `.d.ts` 与实际依赖一致;`devDependencies` 让本仓能编译与测试。profile 不装 peer,所以不会产生副本。
|
|
93
|
+
- **版本要求跟着宿主走。** `dsh-*` 与 `cordis` 精确锁定在宿主声明的版本上,保证复用同一份。`test/publish-manifest.test.ts` 还要求每条 `dsh-*` 锁定版本与 `@deepseek-ai/dsh` peer 的版本一致。
|
|
94
94
|
- 已知上游限制:`@deepseek-ai/dsh-llm` 的声明文件引用了它自己没声明的 `@deepseek-ai/dsh-attachment`。它经 `dsh-session` 传递进来,所以开 `skipLibCheck: false` 的消费方会看到来自那个包的 `TS2307`,不是来自本包。
|
|
95
95
|
|
|
96
96
|
### shared 的版本下限
|
|
97
97
|
|
|
98
|
-
消费者的 profile 可以用 pnpm `overrides`
|
|
98
|
+
消费者的 profile 可以用 pnpm `overrides` 把本包固定在某个版本。固定得低于某个 `./lib/...` 子路径首次出现的版本,宿主启动时就会报 `N entries did not activate` 与 `does not provide an export named ...`:包装得上、永远不加载,而任何单仓 CI 都看不见。要固定版本请先对照本包的 `exports` 与 CHANGELOG。
|
|
99
99
|
|
|
100
100
|
### 随包发什么
|
|
101
101
|
|
|
102
|
-
- `main` 与 `exports` 只指 `dist/`,不存在第二套源码形态的 manifest。Node 对 `node_modules` 里的 `.ts` 直接抛 `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING
|
|
102
|
+
- `main` 与 `exports` 只指 `dist/`,不存在第二套源码形态的 manifest。Node 对 `node_modules` 里的 `.ts` 直接抛 `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`,而发布出去的包正是在那里被载入的,所以产物是必需的。
|
|
103
103
|
- `files` 是 `icon.svg`、`dist`、`config/*.json` 与 `README.zh-CN.md`。npm 永远附带 `package.json`、`LICENSE` 与 `README.md`;中文版显式列出才会一起发。
|
|
104
|
-
- `publishConfig` 只带 `access` 与 `registry
|
|
105
|
-
- `build` 先删除 `dist
|
|
106
|
-
- `prepare`
|
|
107
|
-
-
|
|
104
|
+
- `publishConfig` 只带 `access` 与 `registry`。把入口字段放在那里容易出错:pnpm 发布时会把它合并到顶层,npm 不会——换一个发布器就会发出一个入口指向 tarball 里不存在文件的包。
|
|
105
|
+
- `build` 先删除 `dist/`,再产出运行期产物、配置产物、类型声明与生成的 barrel 声明。两个构建脚本都压缩;`test/dist-size.test.ts` 把运行期产物与配置产物各自限制在预算内。
|
|
106
|
+
- `prepare` 指向 `build`。消费方从 registry 安装时它不执行;它存在是为了让打包、发布与 workspace link 拿到的都是新鲜 `dist/`。
|
|
107
|
+
- 新增模块要同时改五处:`lib/index.ts` 的 barrel、`exports`、`build-host.mjs` 的 `facets` 数组、`tsconfig.build.json` 的 `include`,外加一份 `test/<模块>.test.ts`。漏一处会被「开发」一节的一致性检查判定失败。
|
|
108
108
|
|
|
109
|
-
## <img src="https://api.iconify.design/lucide/circle-check.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" />
|
|
109
|
+
## <img src="https://api.iconify.design/lucide/circle-check.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 确认它在正常工作
|
|
110
110
|
|
|
111
111
|
1. **子路径能解析。** 在消费包里运行 `node -e 'import("@jayyuen66/dsh-plugin-shared/lib/record").then((m) => console.log(typeof m.isRecord))'`,输出 `function`。`ERR_MODULE_NOT_FOUND` 表示依赖没装上;`does not provide an export named ...` 表示装到的那份比你 import 的子路径旧。
|
|
112
|
-
2. **barrel 能解析。**
|
|
113
|
-
3.
|
|
114
|
-
4.
|
|
112
|
+
2. **barrel 能解析。** 换成裸包名做同一件事,应能取到运行期模块。`canonicalize-region-paths` 是唯一刻意不在 barrel 里的模块;构建脚本需要时按子路径取。
|
|
113
|
+
3. **消费方宿主能启动。** 启动声明该插件的 profile。若启动日志出现 `N entries did not activate` 与 `does not provide an export named ...`,说明 profile 的 `overrides` 固定得比包含该子路径的版本低——调高固定版本,不要改插件。
|
|
114
|
+
4. **本地源码改动能反映到消费方。** 消费方载入的是 `dist/`,不是 `lib/`。改完先运行 `pnpm run build`;`test/build-host.test.ts` 会把磁盘上的运行期产物与内存构建逐字节比对,产物过期即报错。"代码改了却没生效"多半是产物过期,不是缓存问题。
|
|
115
115
|
|
|
116
116
|
## <img src="https://api.iconify.design/lucide/circle-slash.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 它不提供什么
|
|
117
117
|
|
|
118
118
|
- **不注册插件。** 没有 `dsh` 字段,没有 `cordis.patch.yml`,`--dump-config` 里没有它这一层;`dsh plugin add` 是错误动作。
|
|
119
|
-
-
|
|
120
|
-
- **没有 HTTP
|
|
119
|
+
- **没有设置卡,也没有客户端部分。** 本包没有浏览器入口,也没有客户端状态。
|
|
120
|
+
- **没有 HTTP 路由,也没有请求信任检查。** 真正注册路由的插件由宿主的 `/api` 网关负责认证、Host / Origin 校验与 body 上限。
|
|
121
121
|
- **没有 CLI 子命令,也没有模型工具。**
|
|
122
|
-
- **不做领域判定。**
|
|
122
|
+
- **不做领域判定。** 它不知道哪些工具算编辑、哪条路径是密钥、预算多大、检查何时该报错。这些属于调用方的插件。
|
|
123
123
|
|
|
124
124
|
## <img src="https://api.iconify.design/lucide/package.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 模块清单
|
|
125
125
|
|
|
@@ -127,81 +127,84 @@ import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
|
|
|
127
127
|
|
|
128
128
|
| 子路径 | 提供什么 |
|
|
129
129
|
| --- | --- |
|
|
130
|
-
| `.` |
|
|
131
|
-
| `./lib/tool-events` | `scanToolEvents` 返回 `calls` / `results` 两张表,PTC
|
|
132
|
-
| `./lib/card-apply` | `claimApply` + `CardApplyCtx
|
|
133
|
-
| `./lib/card-diff` | `diffTouched`:touched 层里哪些字段真的与快照不同,按表单值比较。`undefined`、`null`
|
|
134
|
-
| `./lib/
|
|
135
|
-
| `./lib/
|
|
136
|
-
| `./lib/
|
|
137
|
-
| `./lib/
|
|
138
|
-
| `./lib/
|
|
139
|
-
| `./lib/
|
|
140
|
-
| `./lib/
|
|
141
|
-
| `./lib/
|
|
142
|
-
| `./lib/
|
|
143
|
-
|
|
144
|
-
|
|
130
|
+
| `.` | 下面列出的运行期模块的 barrel。`canonicalize-region-paths` 刻意不进,运行期消费方不必为它增加一次导入;客户端专用的 `card-ui` 同样不进,理由还多一条:根 barrel 不得把 `react` 带进每个消费方 |
|
|
131
|
+
| `./lib/tool-events` | `scanToolEvents` 返回 `calls` / `results` 两张表,PTC 子调用一并合并;`toolEventRowsOf` 处理单个事件,交回的记录已带归一后的 `arguments` 与 `badArguments`。`editPathOf` 返回按 `kind` 判别的 `EditTarget`,只有 `write` / `read-view` 两个分支;路径取不到时为 `undefined`。记录字段一律 `readonly`。同时再导出官方 `SessionEvent` 与两个 PTC 事件类型 |
|
|
132
|
+
| `./lib/card-apply` | `claimApply` + `CardApplyCtx`:保证 apply 幂等的保护逻辑,建立在 `globalThis` 上的**自有**标记(symbol,或外部写入的 `true`)加 `ctx.effect` 清理。标记是进程级的,所以同一页面的第二个 ctx 会被拒绝——这是有意的设计 |
|
|
133
|
+
| `./lib/card-diff` | `diffTouched`:touched 层里哪些字段真的与快照不同,按表单值比较。`undefined`、`null` 与键缺失是同一种值;数组与对象逐元素比较;循环引用也能正常结束 |
|
|
134
|
+
| `./lib/card-ui` | 客户端专用设置卡组件:`CardForm`、`Section`、`FieldGrid`、`StatusBar`、`Field`、`FieldLabel`、`InfoTip`、`ToggleField`、`NumberField`、`TextField`、`ListField`、`SelectField`、`SaveBar`、`splitHint` 与样式字符串 `CARD_UI_CSS`。只用 `createElement`;`react` 是 optional peer,该子路径永不进根 barrel。只用 React 18 运行时 API(`useId`、`useState`、`useEffect`、refs) |
|
|
135
|
+
| `./lib/project-key` | `deriveProjectKey` + `ProjectKeyOptions`:cwd → `尾目录名-<sha256(norm) 前 8 位 hex>`;非字符串、空串或归一后的根目录归入 `default`。`realpathSync` 会解析软链接,失败则退回解析后的路径。Windows 的 `\` 与 `/` 产生同一个键 |
|
|
136
|
+
| `./lib/locale` | `Locale`(官方 `BuiltInLocaleId` 的别名)、`DEFAULT_LOCALE`、`resolveLocale`、`resolveLocalePreference`、`messagesFor`、`MessagesCatalog`,加 `LOCALE_SETTINGS_NAMESPACE` 与 `LOCALE_PREFERENCE_FIELD`。只负责宿主侧字典;客户端用官方 locale 包 |
|
|
137
|
+
| `./lib/lesson-bus` | `settleLessonCall`:同步抛错与异步 rejection 归到同一个 `onFailure` 出口。函数型 thenable 也算可等待;`onFailure` 自己抛错或拒绝时被统一处理,不会分成"抛给调用方"或"未处理拒绝"两种结果 |
|
|
138
|
+
| `./lib/text` | `truncateEnd` 转交官方 `truncateWithoutSplittingSurrogatePair`;`truncateStart` 是官方导出内容里没有的后切形式。两者都按代理对安全切;预算不足一个码元(含 `<= 0` 与 NaN)返回空串 |
|
|
139
|
+
| `./lib/record` | `isRecord`、`fieldOf`、`hasMethods`。`fieldOf` 按设计走原型链(cordis 的服务方法定义在那里);遇到恶意 getter 或 Proxy trap 时改返回 `undefined`,不抛错。`hasMethods` 要求指定的每个方法都可调用,并且不把 `Object.prototype` 的成员当成服务实现 |
|
|
140
|
+
| `./lib/errors` | `errorText` 对齐官方 `@deepseek-ai/dsh-llm` 的 `errorChain` 语义(cause 链、`AggregateError` 成员、空 message 回退 `name`、跨 realm 取 `message`、恶意 getter 改用占位文本),本地零依赖实现。`isSettingsConflict` 由 `code` / `name` 识别宿主的设置冲突特征,跨 realm 抛来的错误也能识别 |
|
|
141
|
+
| `./lib/jsonl` | `shrinkJsonlTail`:JSONL 尾部裁剪的纯逻辑模块。触发策略与错误处理留给调用方;字节预算是否有限由调用方保证 |
|
|
142
|
+
| `./lib/job-outcome` | `jobOutcomeOf` + `SettledProcess` + `JobOutcome` 把已结束的子进程映射成官方作业注册表的结局;`killProc` + `KillableProcess` 释放子进程句柄,对抛错或拒绝的 `kill()` 一律忽略(例如进程已被回收) |
|
|
143
|
+
| `./lib/canonicalize-region-paths` | 构建期字符串处理:把 rolldown 写下的 `//#region <路径>` 改写成与 `process.cwd()` 无关的形态。只提供子路径,是为了让构建脚本取得到它,同时不把运行期 barrel 增大 |
|
|
144
|
+
|
|
145
|
+
`card-ui` 是唯一刻意只走子路径的运行期模块:把它加进 barrel 会让每个 host 侧 importer 都去解析 `react`。
|
|
146
|
+
|
|
147
|
+
## <img src="https://api.iconify.design/lucide/sliders-horizontal.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> config 导出
|
|
145
148
|
|
|
146
149
|
这些是给插件作者用的 lint/test 基线,不是运行期代码。它们依赖你的包自己提供的工具链:
|
|
147
150
|
|
|
148
151
|
| 子路径 | 提供什么 | 你需要装什么 |
|
|
149
152
|
| --- | --- | --- |
|
|
150
|
-
| `./config/oxlint` | `definePluginConfig`、`makeTestFileOverrides`、`OFF_JUSTIFICATIONS` 与共享规则基线 | `oxlint
|
|
151
|
-
| `./config/vitest.base` | `definePackageConfig`:四项 100% 覆盖率阈值、20 秒测试超时、共享覆盖率排除表与用例匹配范围 | `vitest` 与 `@vitest/coverage-v8`(基线把 `coverage.provider`
|
|
153
|
+
| `./config/oxlint` | `definePluginConfig`、`makeTestFileOverrides`、`OFF_JUSTIFICATIONS` 与共享规则基线 | `oxlint`;要启用 sonarjs 规则还要 `eslint-plugin-sonarjs` |
|
|
154
|
+
| `./config/vitest.base` | `definePackageConfig`:四项 100% 覆盖率阈值、20 秒测试超时、共享覆盖率排除表与用例匹配范围 | `vitest` 与 `@vitest/coverage-v8`(基线把 `coverage.provider` 固定为 `"v8"`) |
|
|
152
155
|
| `./config/tsconfig.base.json`、`./config/tsconfig.client.base.json` | 给 `extends` 用的纯 JSON | 什么都不用 |
|
|
153
156
|
|
|
154
|
-
这些工具链包**不**写进 `peerDependencies`:npm 7+ 连 optional peer 都会去满足,而 `oxlint` 自己的 `peerOptional vite-plus` 把 `vitest`
|
|
157
|
+
这些工具链包**不**写进 `peerDependencies`:npm 7+ 连 optional peer 都会去满足,而 `oxlint` 自己的 `peerOptional vite-plus` 把 `vitest` 锁定在一个与 `>=5.0.2` 无交集的版本上,结果是本包的普通 `npm install` 直接 `ERESOLVE` 失败。这条约束因此写在文档里,而不是 manifest 里。
|
|
155
158
|
|
|
156
|
-
- sonarjs 的入口在 `definePluginConfig()` 内部惰性解析。没装 sonarjs 时导入 `./config/oxlint` 仍然成功;既没装 sonarjs、`jsPlugins` 里也没带 sonarjs 入口时,调用 `definePluginConfig()`
|
|
157
|
-
-
|
|
158
|
-
- `definePackageConfig` 拒绝空 `coverageInclude`,也拒绝 `test.coverage`
|
|
159
|
+
- sonarjs 的入口在 `definePluginConfig()` 内部惰性解析。没装 sonarjs 时导入 `./config/oxlint` 仍然成功;既没装 sonarjs、`jsPlugins` 里也没带 sonarjs 入口时,调用 `definePluginConfig()` 才抛错,消息里写明试过哪两个说明符,以及自备入口的方法。
|
|
160
|
+
- 基线里每条关闭为 `off` 的规则都要在 `OFF_JUSTIFICATIONS` 里有 `kind` + `measured` + `note`,否则配置加载即抛。包级例外写进那个包自己的 `offReasons`。
|
|
161
|
+
- `definePackageConfig` 拒绝空 `coverageInclude`,也拒绝 `test.coverage` 块:两者任一都会让四项阈值或统计范围静默消失。要例外就在需要例外的包自己的 `coverage` 覆盖项旁边写明理由。
|
|
159
162
|
|
|
160
163
|
## <img src="https://api.iconify.design/lucide/triangle-alert.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 已知边界
|
|
161
164
|
|
|
162
|
-
|
|
165
|
+
按类登记,每类一条;每条都由测试按现状固定。
|
|
163
166
|
|
|
164
|
-
- **`fieldOf` 刻意走原型链。** 改成 `Object.hasOwn` 会让所有 cordis
|
|
165
|
-
- **`claimApply` 是进程级标记,不是 per-context 锁。** 同一页面上第二个 ctx
|
|
166
|
-
- **`shrinkJsonlTail` 不归一预算。** `NaN` 与 `+Infinity` 会整段关掉收缩;`0`、负数与 `-Infinity` 仍会收缩到最多两行。触发策略(包括"0 表示永不裁剪"
|
|
167
|
-
- **`jobOutcomeOf` 抄的是宿主 bash 工具的文案。**
|
|
168
|
-
- **`editPathOf` 只认一种只读写法。** 只有 `str_replace_editor` 且 `command: "view"` 才是 `read-view`;其余带 `file_path` 的工具一律按 `write`
|
|
167
|
+
- **`fieldOf` 刻意走原型链。** 改成 `Object.hasOwn` 会让所有 cordis 服务方法都取不到。读用户设置值的代码必须改用自有属性判断;`resolveLocalePreference` 就是例子,两个方向都有用例覆盖。
|
|
168
|
+
- **`claimApply` 是进程级标记,不是 per-context 锁。** 同一页面上第二个 ctx 申请同一张卡会被拒绝。需要多 ctx 并存的消费方应自己按 ctx 管理,而不是"修"这道保护。
|
|
169
|
+
- **`shrinkJsonlTail` 不归一预算。** `NaN` 与 `+Infinity` 会整段关掉收缩;`0`、负数与 `-Infinity` 仍会收缩到最多两行。触发策略(包括"0 表示永不裁剪")与错误处理都归调用方。
|
|
170
|
+
- **`jobOutcomeOf` 抄的是宿主 bash 工具的文案。** 测试检查的是 `exit code: N` / `signal: NAME` 这些字面量,不是与上游函数逐字相等——那个函数无法从已发布包里 import。上游哪天改措辞,本包不会报错,显示文本仍是本包自己的。
|
|
171
|
+
- **`editPathOf` 只认一种只读写法。** 只有 `str_replace_editor` 且 `command: "view"` 才是 `read-view`;其余带 `file_path` 的工具一律按 `write` 处理,不再做其他判断。
|
|
169
172
|
|
|
170
173
|
## <img src="https://api.iconify.design/lucide/lock.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 数据与隐私
|
|
171
174
|
|
|
172
175
|
本包被 import 进宿主进程;下表描述的是运行期。
|
|
173
176
|
|
|
174
|
-
|
|
|
177
|
+
| 项目 | 内容 |
|
|
175
178
|
| --- | --- |
|
|
176
179
|
| 文件访问 | `deriveProjectKey` 只对你交给它的那条路径调用 `realpathSync`。失败(`ENOENT` / `EACCES`)退回 `path.resolve`,不外抛。`lib/**` 里没有别的文件访问 |
|
|
177
|
-
|
|
|
180
|
+
| 磁盘写入 | 没有。本包不写文件、不写缓存、不写日志 |
|
|
178
181
|
| 环境变量 | 不读任何环境变量 |
|
|
179
182
|
| 网络 | 没有出站请求,没有 HTTP 服务,没有子进程 |
|
|
180
183
|
| 动态代码 | 没有 `eval`,没有 `new Function`,没有动态 `import()` |
|
|
181
|
-
| 进程内状态 | 唯一的跨实例可变状态是 `claimApply` 在 `globalThis`
|
|
182
|
-
| 日志 | 只有 `lesson-bus` 里的 `onFailure` 自己抛错时才会 `console.error`;测试 setup 会把每条 `[shared/*]`
|
|
184
|
+
| 进程内状态 | 唯一的跨实例可变状态是 `claimApply` 在 `globalThis` 上的标记,effect 卸载或标记失败时恢复 |
|
|
185
|
+
| 日志 | 只有 `lesson-bus` 里的 `onFailure` 自己抛错时才会 `console.error`;测试 setup 会把每条 `[shared/*]` 日志与模板清单比对 |
|
|
183
186
|
|
|
184
187
|
## <img src="https://api.iconify.design/lucide/layers.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 版本与兼容
|
|
185
188
|
|
|
186
|
-
-
|
|
187
|
-
- **`deriveProjectKey` 的哈希算法、8 位切片与尾段空白清洗是冻结的。**
|
|
188
|
-
- **Windows
|
|
189
|
+
- **公共接口一律按语义化版本**:子路径、导出的值符号、导出的类型、错误/原因枚举。`0.x` 的区间只放行 patch 位,所以动这些就是消费方区间里的破坏性变更。
|
|
190
|
+
- **`deriveProjectKey` 的哈希算法、8 位切片与尾段空白清洗是冻结的。** 改动任何一位都会让既有的教训与记忆数据在新键下找不到。
|
|
191
|
+
- **Windows 路径归一改变了键的形态。** 同一目录无论写成 `\` 还是 `/` 都得到 `尾目录名-<哈希>`,盘符根与 UNC 共享根归入 `default`。归一之前在 Windows 上生成的键不再匹配;POSIX 的键逐字节不变,并有字面量断言固定。
|
|
189
192
|
- **`Locale` 是官方 `BuiltInLocaleId` 的别名**,因此跟着宿主的 `LOCALE_IDS` 走,不再是手写的字面量联合。
|
|
190
|
-
- **`errorText` 返回的是展示文本,不是可解析格式。** 它对齐官方 `errorChain` 的语义:cause 链拼成 `外: 内`、`AggregateError` 附 `[e1; e2]`、空 message 回退 `Error.name`、跨 realm 的 Error 取其自带 `message` 而不是 `String()` 的 `"Error: ..."
|
|
191
|
-
- **`editPathOf` 返回按 `kind` 判别的 `EditTarget`,只有 `write` / `read-view`
|
|
192
|
-
- **`./lib/http` 与 `./lib/trust` 已删除。**
|
|
193
|
-
-
|
|
193
|
+
- **`errorText` 返回的是展示文本,不是可解析格式。** 它对齐官方 `errorChain` 的语义:cause 链拼成 `外: 内`、`AggregateError` 附 `[e1; e2]`、空 message 回退 `Error.name`、跨 realm 的 Error 取其自带 `message` 而不是 `String()` 的 `"Error: ..."`。拿它做正则匹配的调用方超出了受支持的用法。
|
|
194
|
+
- **`editPathOf` 返回按 `kind` 判别的 `EditTarget`,只有 `write` / `read-view` 两个分支。** `"skip"` 从来不会被产出,留在类型里只会迫使调用方写永不执行的分支。路径取不到时仍返回所属分支,`path` 为 `undefined`。
|
|
195
|
+
- **`./lib/http` 与 `./lib/trust` 已删除。** 插件家族的设置卡数据通道运行在官方 Typert Remote 上,认证、Host / DNS 重绑定 / Origin 校验与 body 上限由宿主的 `/api` 网关承担。还要自建 `webServer` 路由的插件得自己写这些检查——发布内容不再提供第二套"这个请求准不准"的答案。
|
|
196
|
+
- **声明文件与宿主版本锁定是耦合的。** 升级宿主意味着同步更新本包里精确的 `@deepseek-ai/*` 版本并重新发布;放宽成范围会让消费方载入一份本包没有对着它构建过的类型声明。
|
|
194
197
|
|
|
195
198
|
## <img src="https://api.iconify.design/lucide/circle-help.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 常见问题
|
|
196
199
|
|
|
197
200
|
| 问题 | 回答 |
|
|
198
201
|
| --- | --- |
|
|
199
|
-
| 要不要 `dsh plugin add` 它? |
|
|
200
|
-
| 消费方启动时报 `does not provide an export named ...` 与 `N entries did not activate`。 | profile 提供的那份本包比消费方 import 的版本旧。把 profile 的 `overrides`
|
|
201
|
-
| 改了 `lib/*.ts`,消费方还是旧行为。 | 消费方载入的是 `dist
|
|
202
|
-
| 为什么 `@deepseek-ai/dsh-output-retention` 是必需 peer,其余都是 optional? | 它是唯一按值导入的宿主包(`truncateEnd
|
|
203
|
-
| 为什么 `oxlint` 不写成 `peerDependency`? | npm 7+ 会去满足它,而 `oxlint` 的 optional `vite-plus` peer 把 `vitest`
|
|
204
|
-
| 想把某个 helper
|
|
202
|
+
| 要不要 `dsh plugin add` 它? | 不要。它没有配置层也没有客户端部分,启用不了任何东西。正确做法是声明成插件包的依赖。 |
|
|
203
|
+
| 消费方启动时报 `does not provide an export named ...` 与 `N entries did not activate`。 | profile 提供的那份本包比消费方 import 的版本旧。把 profile 的 `overrides` 版本调高到包含该子路径的版本;对照本包的 `exports` 与 CHANGELOG。任何单仓 CI 都看不见这个状态。 |
|
|
204
|
+
| 改了 `lib/*.ts`,消费方还是旧行为。 | 消费方载入的是 `dist/`。先运行 `pnpm run build`;`test/build-host.test.ts` 会在运行期产物过期时判定失败。workspace link 不会绕过产物。 |
|
|
205
|
+
| 为什么 `@deepseek-ai/dsh-output-retention` 是必需 peer,其余都是 optional? | 它是唯一按值导入的宿主包(`truncateEnd`)。其余都是类型声明,唯一的例外是裸 `@deepseek-ai/dsh` peer——它只声明宿主版本线。 |
|
|
206
|
+
| 为什么 `oxlint` 不写成 `peerDependency`? | npm 7+ 会去满足它,而 `oxlint` 的 optional `vite-plus` peer 把 `vitest` 锁定在基线所需范围之外,普通安装会得到 `ERESOLVE`。这条要求因此写在文档里。 |
|
|
207
|
+
| 想把某个 helper 收录进来? | 先确认那份函数体在两个以上包里逐字重复。带领域判断的部分留在插件里。 |
|
|
205
208
|
|
|
206
209
|
## <img src="https://api.iconify.design/lucide/wrench.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 开发
|
|
207
210
|
|
|
@@ -209,18 +212,18 @@ import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
|
|
|
209
212
|
pnpm install
|
|
210
213
|
pnpm run check # typecheck → lint → build → test → 格式检查
|
|
211
214
|
pnpm test # vitest,带覆盖率
|
|
212
|
-
pnpm run build #
|
|
215
|
+
pnpm run build # 运行期产物、配置产物、声明文件
|
|
213
216
|
```
|
|
214
217
|
|
|
215
|
-
-
|
|
216
|
-
-
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
220
|
-
-
|
|
221
|
-
- **发布形态**:`test/publish-manifest.test.ts` 要求入口目标都在 `files` 覆盖内、入口指向产物而不是源码、`publishConfig` 不带入口字段、每条 peer 都在 `peerDependenciesMeta`
|
|
222
|
-
- 覆盖率四项阈值都是 **100%**(lines / statements / functions / branches),所以新增分支必须有能走到它的用例。`test/vitest-base.test.ts` 拒绝空 `coverageInclude
|
|
223
|
-
- CI 在 Linux、Windows 与 macOS
|
|
218
|
+
- 测试按「stub 覆盖不到的那类缺陷」分层:
|
|
219
|
+
- **模块行为**:每个运行期模块一份 `test/<模块>.test.ts`。
|
|
220
|
+
- **接口一致性(surface parity)**:`test/index.test.ts`(barrel ↔ `lib/` 的值导出)、`test/publish-manifest.test.ts`(`exports` ↔ `lib/` 双向)与 `test/build-host.test.ts`(`build-host.mjs` 的 `facets` ↔ `lib/`)。三条检查各抓一种另外两条发现不了的漏项。
|
|
221
|
+
- **产物检查**:`test/build-host.test.ts` 把磁盘上的运行期产物与内存构建逐字节比对;`test/dist-size.test.ts` 把运行期产物与配置产物各自限制在字节预算内;同一份测试还固定官方值导入必须保持裸说明符,并拒绝为 `dsh-jobs` / `dsh-shell` 留下运行期说明符。
|
|
222
|
+
- **配置检查**:`test/config-oxlint.test.ts` 把 vitest 规则快照与 `oxlint --rules` 的实际输出对照,并要求每条 `off` 都有依据;`test/vitest-base.test.ts` 固定合并行为,保证四项阈值与统计范围不会被局部改配抹掉。
|
|
223
|
+
- **日志记录**:`test/setup-logs.ts` 接管 `console.*`;每条 `[shared/*]` 日志都要匹配 `test/log-templates.ts` 里的模板片段,否则测试立即失败。`test/log-templates.test.ts` 双向校验,增删日志却不改记录清单都会失败。
|
|
224
|
+
- **发布形态**:`test/publish-manifest.test.ts` 要求入口目标都在 `files` 覆盖内、入口指向产物而不是源码、`publishConfig` 不带入口字段、每条 peer 都在 `peerDependenciesMeta` 里明确标注、`dsh-*` 版本精确且统一,并要求源码、文档与产物里没有日期、绝对路径、家目录指涉或只在协作当时成立的措辞。
|
|
225
|
+
- 覆盖率四项阈值都是 **100%**(lines / statements / functions / branches),所以新增分支必须有能走到它的用例。`test/vitest-base.test.ts` 拒绝空 `coverageInclude`,因为那会让四项阈值在空统计范围上全部通过。
|
|
226
|
+
- CI 在 Linux、Windows 与 macOS 上运行同一组检查,Node 版本矩阵覆盖 22 与 24,并有一周一次的定时运行来发现上游变化。安装矩阵会分别用 npm 与 pnpm 打包,在干净目录里不带 peer 解析选项安装,逐子路径 import,并在两种 `skipLibCheck` 设置下对一小段消费方导入代码做类型检查。
|
|
224
227
|
|
|
225
228
|
## <img src="https://api.iconify.design/lucide/scale.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 许可
|
|
226
229
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"//": "target 不覆盖:与官方一致(deepseek-harness/tsconfig.base.json 是 es2024,client 层只加 lib)。本仓的 client 产物由 rolldown 打,构建脚本里没有 target,也没有读 tsconfig
|
|
2
|
+
"//": "target 不覆盖:与官方一致(deepseek-harness/tsconfig.base.json 是 es2024,client 层只加 lib)。本仓的 client 产物由 rolldown 打,构建脚本里没有 target,也没有读 tsconfig,因此这里的 target 只影响类型面,不影响产物语法。旧基线把它固定在 ES2020 是继承来的写法,实测无产物后果,只会把 Promise.withResolvers 一类 ES2024 API 判成 TS2550,迫使包级添加 lint 例外。types 置空刻意关掉父基线的 node @types 自动加载:纯浏览器 client 是对的;下游 client 源码若用到 Node 全局或需要 vite/client 的声明,须在自己的 tsconfig 显式补 types,报错点(TS2588/TS2304 一类)离这里很远,别把 node 加回这份基线。",
|
|
3
3
|
"extends": "./tsconfig.base.json",
|
|
4
4
|
"compilerOptions": {
|
|
5
5
|
"module": "ESNext",
|
package/dist/card-ui.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import{createElement as e,useCallback as t,useEffect as n,useId as r,useLayoutEffect as i,useMemo as a,useRef as o,useState as s}from"react";const c=`
|
|
2
|
+
.dsu-form{display:flex;flex-direction:column}
|
|
3
|
+
.dsu-section{margin-top:24px;padding-top:24px;border-top:.5px solid var(--dsw-alias-border-l1)}
|
|
4
|
+
.dsu-form>.dsu-section:first-child{margin-top:0;padding-top:0;border-top:0}
|
|
5
|
+
.dsu-sectionTitle{margin:0;font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}
|
|
6
|
+
.dsu-sectionHint{margin:4px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
|
|
7
|
+
.dsu-statusbar{display:flex;flex-wrap:wrap;gap:4px 8px;margin-top:8px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
|
|
8
|
+
.dsu-field{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,360px);column-gap:32px;align-items:center;padding:12px 0;border-top:.5px solid var(--dsw-alias-border-l2)}
|
|
9
|
+
.dsu-fieldMeta{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
10
|
+
.dsu-labelRow{display:flex;align-items:center;gap:6px;min-width:0}
|
|
11
|
+
.dsu-label{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
|
|
12
|
+
.dsu-labelMono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
|
|
13
|
+
.dsu-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
|
|
14
|
+
.dsu-control{min-width:0;display:flex;align-items:center;gap:8px}
|
|
15
|
+
.dsu-fieldWide{grid-template-columns:minmax(0,1fr);align-items:start}
|
|
16
|
+
.dsu-fieldWide .dsu-control{width:100%;max-width:720px;margin-top:2px}
|
|
17
|
+
.dsu-help{display:inline-flex;align-items:center;gap:6px;min-width:0}
|
|
18
|
+
.dsu-helpBtn{appearance:none;border:0;background:transparent;padding:0;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);cursor:help;border-radius:50%;flex:none}
|
|
19
|
+
.dsu-help:hover .dsu-helpBtn,.dsu-helpBtn:focus-visible{color:var(--dsw-alias-label-primary)}
|
|
20
|
+
.dsu-helpBtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
|
|
21
|
+
.dsu-tip{position:fixed;z-index:60;box-sizing:border-box;max-width:min(340px,calc(100vw - 16px));max-height:calc(100vh - 16px);overflow:auto;overflow-wrap:anywhere;padding:10px 12px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);box-shadow:0 12px 32px rgb(0 0 0 / 45%);font-size:12px;line-height:1.6;font-weight:400;color:var(--dsw-alias-label-secondary)}
|
|
22
|
+
.dsu-input{width:100%;height:34px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
|
|
23
|
+
.dsu-input:focus-visible{outline:none;border-color:var(--dsw-alias-state-business-primary)}
|
|
24
|
+
.dsu-input[aria-invalid="true"]{border-color:var(--dsw-alias-state-error-primary)}
|
|
25
|
+
.dsu-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
26
|
+
.dsu-input::placeholder{color:var(--dsw-alias-label-tertiary)}
|
|
27
|
+
.dsu-textarea{height:auto;min-height:96px;padding:8px 12px;line-height:1.6;resize:vertical}
|
|
28
|
+
.dsu-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
29
|
+
.dsu-numberWrap{display:flex;flex-direction:column;gap:4px;width:100%;min-width:0}
|
|
30
|
+
.dsu-number{display:flex;align-items:center;gap:8px;min-width:0}
|
|
31
|
+
.dsu-number .dsu-input{min-width:0}
|
|
32
|
+
.dsu-unit{flex:none;font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
33
|
+
.dsu-error{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary)}
|
|
34
|
+
.dsu-switch{appearance:none;width:36px;height:20px;border-radius:999px;border:0;padding:2px;background:var(--dsw-alias-border-l3);display:flex;align-items:center;cursor:pointer;flex:none;transition:background-color .16s}
|
|
35
|
+
.dsu-knob{width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary);transition:transform .16s,background-color .16s}
|
|
36
|
+
.dsu-switch[aria-checked="true"]{background:var(--dsw-alias-label-primary)}
|
|
37
|
+
.dsu-switch[aria-checked="true"] .dsu-knob{transform:translateX(16px);background:var(--dsw-alias-label-primary-foreground)}
|
|
38
|
+
.dsu-switch:disabled{opacity:.4;cursor:not-allowed}
|
|
39
|
+
.dsu-switch:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
40
|
+
.dsu-selectWrap{position:relative;width:100%;min-width:0}
|
|
41
|
+
.dsu-select{display:flex;align-items:center;justify-content:space-between;gap:8px;text-align:left;cursor:pointer}
|
|
42
|
+
.dsu-select[aria-expanded="true"]{border-color:var(--dsw-alias-state-business-primary)}
|
|
43
|
+
.dsu-selectValue{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
44
|
+
.dsu-chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}
|
|
45
|
+
.dsu-select[aria-expanded="true"] .dsu-chevron{transform:rotate(180deg)}
|
|
46
|
+
.dsu-menu{position:fixed;z-index:70;padding:4px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:12px;box-shadow:0 12px 32px rgb(0 0 0 / 50%);max-height:280px;overflow:auto}
|
|
47
|
+
.dsu-option{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:1.4;width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px;border-radius:8px;cursor:pointer;text-align:left}
|
|
48
|
+
.dsu-option:hover,.dsu-option[data-active="true"]{background:var(--dsw-alias-bg-layer-3)}
|
|
49
|
+
.dsu-option:disabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}
|
|
50
|
+
.dsu-option:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
|
|
51
|
+
.dsu-optionGroup{padding:6px 10px 2px;font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary)}
|
|
52
|
+
.dsu-check{opacity:0;flex:none}
|
|
53
|
+
.dsu-option[aria-selected="true"] .dsu-check{opacity:1}
|
|
54
|
+
.dsu-savebar{display:flex;align-items:center;gap:8px;padding:12px 0;margin-top:24px;border-top:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base)}
|
|
55
|
+
.dsu-savebar[data-dirty="true"]{position:sticky;bottom:0;z-index:2;box-shadow:0 -12px 24px rgb(0 0 0 / 35%)}
|
|
56
|
+
.dsu-btn{appearance:none;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:1.5;border-radius:var(--dsw-radius-md);padding:5px 14px;cursor:pointer;transition:background-color .12s,border-color .12s,color .12s,filter .12s}
|
|
57
|
+
.dsu-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l3)}
|
|
58
|
+
.dsu-btn:active:not(:disabled){background:var(--dsw-alias-bg-layer-1)}
|
|
59
|
+
.dsu-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
|
|
60
|
+
.dsu-btn:disabled{opacity:.4;cursor:not-allowed}
|
|
61
|
+
.dsu-btnPrimary{background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
62
|
+
.dsu-btnPrimary:hover:not(:disabled){background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);filter:brightness(.9)}
|
|
63
|
+
.dsu-btnPrimary:active:not(:disabled){background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);filter:brightness(.84)}
|
|
64
|
+
.dsu-btnDanger{color:var(--dsw-alias-state-error-primary)}
|
|
65
|
+
.dsu-btnDanger:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2)}
|
|
66
|
+
.dsu-status{margin-left:auto;font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
67
|
+
.dsu-statusError{color:var(--dsw-alias-state-error-primary)}
|
|
68
|
+
.dsu-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(200px,100%),1fr));gap:0 24px}
|
|
69
|
+
.dsu-grid>.dsu-field{display:flex;flex-direction:column;align-items:stretch;gap:6px;padding:10px 0;border-top:.5px solid var(--dsw-alias-border-l1)}
|
|
70
|
+
.dsu-grid>.dsu-field .dsu-control{width:100%}
|
|
71
|
+
@media (max-width:640px){
|
|
72
|
+
.dsu-field{grid-template-columns:minmax(0,1fr);align-items:start}
|
|
73
|
+
.dsu-control{width:100%;max-width:720px}
|
|
74
|
+
.dsu-section{margin-top:20px;padding-top:20px}
|
|
75
|
+
}
|
|
76
|
+
@media (prefers-reduced-motion:reduce){
|
|
77
|
+
.dsu-switch,.dsu-knob,.dsu-chevron,.dsu-btn{transition:none}
|
|
78
|
+
}
|
|
79
|
+
`;function l(e,t=30){return e.length<=t?{hint:e}:{detail:e}}function u(t){return e(`div`,{className:`dsu-form`},t.children)}function d(t){return e(`section`,{className:`dsu-section`},e(`h2`,{className:`dsu-sectionTitle`},t.title),t.hint===void 0?null:e(`p`,{className:`dsu-sectionHint`},t.hint),t.children)}function f(t){return e(`div`,{className:`dsu-grid`},t.children)}function p(t){if(t.items.length===0)return null;let n=t.items.map((t,n)=>e(`span`,{key:n},n===0?t:`· ${t}`));return e(`div`,{className:`dsu-statusbar`},...n)}function m(a){let c=r(),[l,u]=s(!1),[d,f]=s(!1),p=o(!1),m=o(!1),h=o(!1),g=o(null),_=o(null),v=o(null),y=t(()=>{v.current!==null&&(clearTimeout(v.current),v.current=null)},[]),b=t(()=>{let e=g.current,t=_.current;
|
|
80
|
+
/* v8 ignore next 3 -- place 只在 open 的 layout effect 与挂载期监听器里调用;refs 为空只可能是卸载竞态 */
|
|
81
|
+
if(e===null||t===null)return;let n=e.getBoundingClientRect(),r=t.offsetWidth,i=t.offsetHeight,{left:a}=n,o=n.bottom+8;o+i>window.innerHeight-8&&(o=n.top-i-8),a+r>window.innerWidth-8&&(a=window.innerWidth-r-8);let s=Math.max(8,window.innerWidth-r-8),c=Math.max(8,window.innerHeight-i-8);t.style.left=`${Math.min(Math.max(8,a),s)}px`,t.style.top=`${Math.min(Math.max(8,o),c)}px`},[]),x=t(()=>{y(),h.current=!1,f(!1),u(!1)},[y]),S=t(()=>{y(),u(!0)},[y]),C=t(()=>{y(),!(p.current||m.current||h.current)&&(v.current=setTimeout(()=>{
|
|
82
|
+
/* v8 ignore next 3 -- 任何重新进入/钉住都会 cancelHide;这次复查只防未来的异步路径 */
|
|
83
|
+
v.current=null,p.current||m.current||h.current||u(!1)},120))},[y]),w=t(e=>{h.current=e,f(e)},[]);i(()=>{if(!l)return()=>{};b();let e=e=>{let{target:t}=e;t instanceof Node&&_.current?.contains(t)===!0||(h.current?b():x())},t=()=>{b()};return window.addEventListener(`scroll`,e,!0),window.addEventListener(`resize`,t),()=>{window.removeEventListener(`scroll`,e,!0),window.removeEventListener(`resize`,t)}},[l,a.detail,b,x]),n(()=>{if(!l)return()=>{};let e=e=>{e.key===`Escape`&&x()};return document.addEventListener(`keydown`,e),()=>{document.removeEventListener(`keydown`,e)}},[l,x]),n(()=>y,[y]);let T=e(`button`,{ref:g,type:`button`,className:`dsu-helpBtn`,"aria-label":`${a.label} 说明`,"aria-describedby":c,"aria-pressed":d,onClick:()=>{h.current?x():(w(!0),S())}},e(`svg`,{width:12,height:12,viewBox:`0 0 14 14`,"aria-hidden":!0},e(`circle`,{cx:7,cy:7,r:6,fill:`none`,stroke:`currentColor`,strokeWidth:1.2}),e(`path`,{d:`M7 6.2v4`,stroke:`currentColor`,strokeWidth:1.3,strokeLinecap:`round`}),e(`circle`,{cx:7,cy:4.3,r:.8,fill:`currentColor`}))),E=e(`div`,{ref:_,id:c,role:`tooltip`,className:`dsu-tip`,hidden:!l},a.detail),D=a.showLabel===!1?null:e(`span`,{className:`dsu-label${a.compact===!0?` dsu-labelMono`:``}`},a.label);return e(`span`,{className:`dsu-help`,onMouseEnter:()=>{p.current=!0,S()},onMouseLeave:()=>{p.current=!1,C()},onFocus:()=>{m.current=!0,S()},onBlur:e=>{let t=e.relatedTarget;t instanceof Node&&e.currentTarget.contains(t)||(m.current=!1,C())}},D,T,E)}function h(t){let n=`dsu-label${t.compact===!0?` dsu-labelMono`:``}`,r=t.controlId===void 0?e(`span`,{className:n},t.label):e(`label`,{className:n,htmlFor:t.controlId},t.label);return e(`div`,{className:`dsu-labelRow`},r,t.detail===void 0?null:e(m,{label:t.label,detail:t.detail,showLabel:!1}))}function g(t){let n=t.controlId===void 0?void 0:`${t.controlId}-hint`;return e(`div`,{className:`dsu-field${t.wide===!0?` dsu-fieldWide`:``}`},e(`div`,{className:`dsu-fieldMeta`},e(h,{label:t.label,detail:t.detail,compact:t.compact,controlId:t.controlId}),t.hint===void 0?null:e(`p`,{className:`dsu-hint`,id:n},t.hint)),e(`div`,{className:`dsu-control`},t.children))}function _(t){let n=r(),i=t.hint===void 0?void 0:`${n}-hint`;return e(g,{...t,controlId:n},e(`button`,{type:`button`,role:`switch`,id:n,className:`dsu-switch`,"aria-checked":t.checked,"aria-describedby":i,"data-field":t.field,disabled:t.disabled===!0,onClick:()=>{t.onChange(!t.checked)}},e(`span`,{className:`dsu-knob`})))}function v(t){let i=typeof t.value==`number`&&Number.isFinite(t.value)?String(t.value):``,[a,c]=s(i),[l,u]=s(!1),d=o(void 0),f=r(),p=r(),m=[t.hint===void 0?void 0:`${f}-hint`,l?p:void 0].filter(e=>e!==void 0),h=m.length===0?void 0:m.join(` `);n(()=>{t.value!==d.current&&(d.current=t.value,c(i),u(!1))},[t.value,i]);let _=e=>{let n=t.min??-1/0,r=t.max??1/0;if(!Number.isFinite(e)||e<n||e>r)return!1;let{step:i}=t;if(i===void 0||i<=0)return!0;let a=(e-(t.min??0))/i;return Math.abs(a-Math.round(a))<1e-9},v=e=>{if(c(e),e===``){u(!1),d.current=void 0,t.onChange(void 0);return}let n=Number(e);_(n)?(u(!1),d.current=n,t.onChange(n)):u(!0)},y=()=>{a===``||l||c(String(Number(a)))};return e(g,{...t,controlId:f},e(`div`,{className:`dsu-numberWrap`},e(`div`,{className:`dsu-number`},e(`input`,{className:`dsu-input`,type:`number`,inputMode:`numeric`,id:f,value:a,min:t.min,max:t.max,step:t.step,placeholder:t.placeholder,"aria-invalid":l||void 0,"aria-describedby":h,"data-field":t.field,disabled:t.disabled===!0,onChange:e=>{v(e.target.value)},onBlur:y}),t.unit===void 0?null:e(`span`,{className:`dsu-unit`},t.unit)),l?e(`p`,{className:`dsu-error`,id:p},t.invalidHint):null))}function y(t){let[i,a]=s(t.value),[o,c]=s(!1),l=r(),u=t.hint===void 0?void 0:`${l}-hint`;n(()=>{o||a(t.value)},[t.value,o]);let d={id:l,className:`dsu-input${t.multiline===!0?` dsu-textarea`:``}${t.monospace===!0?` dsu-mono`:``}`,value:i,placeholder:t.placeholder,"aria-describedby":u,"data-field":t.field,disabled:t.disabled===!0,onFocus:()=>{c(!0)},onBlur:()=>{c(!1)},onChange:e=>{a(e.target.value),t.onChange(e.target.value)}};return e(g,{...t,controlId:l},t.multiline===!0?e(`textarea`,d):e(`input`,d))}function b(t){let i=t.separator??`comma`,a=t.value.join(i===`newline`?`
|
|
84
|
+
`:`, `),[o,c]=s(a),[l,u]=s(!1),d=r(),f=t.hint===void 0?void 0:`${d}-hint`;n(()=>{l||c(a)},[a,l]);let p=e=>e.split(i===`newline`?`
|
|
85
|
+
`:`,`).map(e=>e.trim()).filter(e=>e.length>0),m={id:d,className:`dsu-input${i===`newline`?` dsu-textarea dsu-mono`:``}`,value:o,placeholder:t.placeholder,"aria-describedby":f,"data-field":t.field,disabled:t.disabled===!0,onFocus:()=>{u(!0)},onBlur:()=>{u(!1)},onChange:e=>{c(e.target.value),t.onChange(p(e.target.value))}};return e(g,{...t,controlId:d},e(i===`newline`?`textarea`:`input`,m))}function x(c){let[l,u]=s(!1),[d,f]=s(0),p=o(null),m=o(null),h=r(),_=r(),v=c.hint===void 0?void 0:`${_}-hint`,{disabled:y,value:b,options:x,onChange:S}=c,C=a(()=>x.map((e,t)=>({option:e,index:t})).filter(e=>e.option.disabled!==!0),[x]),w=x.find(e=>e.value===b)?.label??b,T=x[d],E=T!==void 0&&T.disabled!==!0,D=t(()=>{let e=p.current,t=m.current;
|
|
86
|
+
/* v8 ignore next 3 -- place 只在 open 的 layout effect 里调用;refs 为空只可能是卸载竞态 */
|
|
87
|
+
if(e===null||t===null)return;let n=e.getBoundingClientRect(),r=Math.min(n.width,window.innerWidth-16),{left:i}=n;i+r>window.innerWidth-8&&(i=window.innerWidth-r-8);let a=t.offsetHeight,o=n.bottom+6;o+a>window.innerHeight-8&&(o=n.top-a-6);let s=Math.max(8,window.innerWidth-r-8),c=Math.max(8,window.innerHeight-a-8);t.style.width=`${r}px`,t.style.left=`${Math.min(Math.max(8,i),s)}px`,t.style.top=`${Math.min(Math.max(8,o),c)}px`},[]),O=t(()=>{u(!1)},[]),k=t(()=>{if(y===!0)return;let e=C.find(e=>e.option.value===b)??C.at(0);e!==void 0&&(f(e.index),u(!0))},[y,b,C]),A=t(e=>{
|
|
88
|
+
/* v8 ignore next 3 -- 浮层只能经由 openMenu 打开;组件禁用时打不开 */
|
|
89
|
+
if(y===!0)return;let t=x[e];
|
|
90
|
+
/* v8 ignore next 3 -- disabled 选项在浏览器里不派发 click;越界下标由调用面保证 */
|
|
91
|
+
t!==void 0&&t.disabled!==!0&&(S(t.value),O(),p.current?.focus())},[y,x,S,O]),j=t(e=>{let t=C.findIndex(e=>e.index===d);
|
|
92
|
+
/* v8 ignore next 5 -- open 时归一 effect 已保证 enabled 非空且 active 落在可用项上 */
|
|
93
|
+
if(t===-1||C.length===0)return;let n=(t+e+C.length)%C.length,r=C[n];
|
|
94
|
+
/* v8 ignore next 3 -- next 由取模保证在界内 */
|
|
95
|
+
r!==void 0&&f(r.index)},[d,C]);i(()=>{if(!l)return()=>{};D();let e=e=>{let t=e.target;m.current?.contains(t)!==!0&&p.current?.contains(t)!==!0&&O()},t=e=>{let{target:t}=e;t instanceof Node&&m.current?.contains(t)===!0||O()},n=()=>{D()};return document.addEventListener(`pointerdown`,e,!0),window.addEventListener(`scroll`,t,!0),window.addEventListener(`resize`,n),()=>{document.removeEventListener(`pointerdown`,e,!0),window.removeEventListener(`scroll`,t,!0),window.removeEventListener(`resize`,n)}},[l,c.options,D,O]),n(()=>{if(!l)return;if(y===!0||C.length===0){O();return}if(C.some(e=>e.index===d))return;let e=C.find(e=>e.option.value===b)??C.at(0);
|
|
96
|
+
/* v8 ignore next 3 -- enabled 非空在上一分支已保证 */
|
|
97
|
+
e!==void 0&&f(e.index)},[l,y,b,x,d,O,C]),i(()=>{if(!l)return;let e=m.current;
|
|
98
|
+
/* v8 ignore next 2 -- 同上,menu 在 open 的 commit 之后必然已挂上 */
|
|
99
|
+
if(e===null)return;let t=e.querySelector(`[data-active="true"]`);
|
|
100
|
+
/* v8 ignore next 2 -- active 已由归一 effect 保证落在可用项上 */
|
|
101
|
+
t!==null&&(t.offsetTop<e.scrollTop?e.scrollTop=t.offsetTop:t.offsetTop+t.offsetHeight>e.scrollTop+e.clientHeight&&(e.scrollTop=t.offsetTop+t.offsetHeight-e.clientHeight))},[l,d,x]);let M=e=>{if(e.key===`ArrowDown`||e.key===`ArrowUp`){if(e.preventDefault(),!l){k();return}j(e.key===`ArrowDown`?1:-1)}else if(e.key===`Home`||e.key===`End`){if(e.preventDefault(),l||k(),C.length===0)return;let t=e.key===`Home`?C.at(0):C.at(-1);
|
|
102
|
+
/* v8 ignore next 3 -- enabled 为空已在上一条返回 */
|
|
103
|
+
if(t===void 0)return;f(t.index)}else if(e.key===`Enter`||e.key===` `){if(e.preventDefault(),!l){k();return}A(d)}else(e.key===`Tab`||e.key===`Escape`)&&O()},N=[],P;for(let[t,n]of x.entries())n.group!==P&&(n.group!==void 0&&N.push(e(`div`,{key:`group-${t}-${n.group}`,className:`dsu-optionGroup`,role:`presentation`},n.group)),P=n.group),N.push(e(`button`,{key:`option-${n.value}`,type:`button`,role:`option`,id:`${h}-${t}`,className:`dsu-option`,tabIndex:-1,"aria-selected":n.value===c.value,"data-active":t===d,disabled:n.disabled===!0,onMouseDown:e=>{e.preventDefault()},onClick:()=>{A(t)},onMouseEnter:()=>{
|
|
104
|
+
/* v8 ignore next -- happy-dom 对 disabled 元素不派发 mouse 事件;浏览器实现各有差异,保留禁用检查 */
|
|
105
|
+
n.disabled!==!0&&f(t)}},e(`span`,null,n.label),e(`svg`,{className:`dsu-check`,width:14,height:14,viewBox:`0 0 14 14`,"aria-hidden":!0},e(`path`,{d:`M2.5 7.5l3 3 6-7`,fill:`none`,stroke:`currentColor`,strokeWidth:1.5,strokeLinecap:`round`,strokeLinejoin:`round`}))));return e(g,{...c,controlId:_},e(`div`,{className:`dsu-selectWrap`,onBlur:e=>{let t=e.relatedTarget;t instanceof Node&&e.currentTarget.contains(t)||O()}},e(`button`,{ref:p,type:`button`,id:_,className:`dsu-select dsu-input`,role:`combobox`,"aria-haspopup":`listbox`,"aria-expanded":l,"aria-controls":`${h}-list`,"aria-activedescendant":l&&E?`${h}-${d}`:void 0,"aria-describedby":v,"data-field":c.field,disabled:y===!0||x.length===0,onClick:()=>{l?O():k()},onKeyDown:M},e(`span`,{className:`dsu-selectValue`},w),e(`svg`,{className:`dsu-chevron`,width:14,height:14,viewBox:`0 0 14 14`,"aria-hidden":!0},e(`path`,{d:`M3 5l4 4 4-4`,fill:`none`,stroke:`currentColor`,strokeWidth:1.5,strokeLinecap:`round`,strokeLinejoin:`round`}))),e(`div`,{ref:m,id:`${h}-list`,role:`listbox`,className:`dsu-menu`,"aria-label":c.label,hidden:!l},...N)))}function S(t){let n=!t.writable||t.busy||!t.dirty,r;return r=t.error===null?t.writable?t.dirty?t.dirtyLabel:t.cleanLabel:t.readOnlyLabel:t.error,e(`div`,{className:`dsu-savebar`,"data-dirty":t.dirty},e(`button`,{type:`button`,className:`dsu-btn dsu-btnPrimary`,"data-field":`save`,disabled:n,onClick:t.onSave},t.busy?t.savingLabel:t.saveLabel),e(`button`,{type:`button`,className:`dsu-btn`,"data-field":`discard`,disabled:n,onClick:t.onDiscard},t.revertLabel),e(`span`,{className:t.error===null?`dsu-status`:`dsu-status dsu-statusError`},r))}export{c as CARD_UI_CSS,u as CardForm,g as Field,f as FieldGrid,h as FieldLabel,m as InfoTip,b as ListField,v as NumberField,S as SaveBar,d as Section,x as SelectField,p as StatusBar,y as TextField,_ as ToggleField,l as splitHint};
|