@jayyuen66/dsh-plugin-shared 0.2.3 → 0.2.4

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/dist/client.js ADDED
@@ -0,0 +1,994 @@
1
+ // GENERATED by build-client.mjs — edit lib/client-entry.ts, then run `node build-client.mjs`.
2
+ window.__ModuleLoader__.load({
3
+ id: "@jayyuen66/dsh-plugin-shared",
4
+ factory: (require) => {
5
+ 'use strict';
6
+ const module = { exports: {} };
7
+ let exports = module.exports;
8
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
+ let react = require("react");
10
+ // lib/card-ui/styles.ts
11
+ /** 设置卡统一样式。颜色/圆角全部走宿主主题令牌。 */
12
+ const CARD_UI_CSS = `
13
+ .dsu-form{display:flex;flex-direction:column}
14
+ .dsu-section{margin-top:24px;padding-top:24px;border-top:.5px solid var(--dsw-alias-border-l1)}
15
+ .dsu-form>.dsu-section:first-child{margin-top:0;padding-top:0;border-top:0}
16
+ .dsu-sectionTitle{margin:0;font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}
17
+ .dsu-sectionHint{margin:4px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
18
+ .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)}
19
+ .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)}
20
+ .dsu-fieldMeta{display:flex;flex-direction:column;gap:4px;min-width:0}
21
+ .dsu-labelRow{display:flex;align-items:center;gap:6px;min-width:0}
22
+ .dsu-label{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
23
+ .dsu-labelMono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
24
+ .dsu-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
25
+ .dsu-control{min-width:0;display:flex;align-items:center;gap:8px}
26
+ .dsu-fieldWide{grid-template-columns:minmax(0,1fr);align-items:start}
27
+ .dsu-fieldWide .dsu-control{width:100%;max-width:720px;margin-top:2px}
28
+ .dsu-help{display:inline-flex;align-items:center;gap:6px;min-width:0}
29
+ .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}
30
+ .dsu-help:hover .dsu-helpBtn,.dsu-helpBtn:focus-visible{color:var(--dsw-alias-label-primary)}
31
+ .dsu-helpBtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
32
+ .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)}
33
+ .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}
34
+ .dsu-input:focus-visible{outline:none;border-color:var(--dsw-alias-state-business-primary)}
35
+ .dsu-input[aria-invalid="true"]{border-color:var(--dsw-alias-state-error-primary)}
36
+ .dsu-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
37
+ .dsu-input::placeholder{color:var(--dsw-alias-label-tertiary)}
38
+ .dsu-textarea{height:auto;min-height:96px;padding:8px 12px;line-height:1.6;resize:vertical}
39
+ .dsu-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
40
+ .dsu-numberWrap{display:flex;flex-direction:column;gap:4px;width:100%;min-width:0}
41
+ .dsu-number{display:flex;align-items:center;gap:8px;min-width:0}
42
+ .dsu-number .dsu-input{min-width:0}
43
+ .dsu-unit{flex:none;font-size:12px;color:var(--dsw-alias-label-tertiary)}
44
+ .dsu-error{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary)}
45
+ .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}
46
+ .dsu-knob{width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary);transition:transform .16s,background-color .16s}
47
+ .dsu-switch[aria-checked="true"]{background:var(--dsw-alias-label-primary)}
48
+ .dsu-switch[aria-checked="true"] .dsu-knob{transform:translateX(16px);background:var(--dsw-alias-label-primary-foreground)}
49
+ .dsu-switch:disabled{opacity:.4;cursor:not-allowed}
50
+ .dsu-switch:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
51
+ .dsu-selectWrap{position:relative;width:100%;min-width:0}
52
+ .dsu-select{display:flex;align-items:center;justify-content:space-between;gap:8px;text-align:left;cursor:pointer}
53
+ .dsu-select[aria-expanded="true"]{border-color:var(--dsw-alias-state-business-primary)}
54
+ .dsu-selectValue{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
55
+ .dsu-chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}
56
+ .dsu-select[aria-expanded="true"] .dsu-chevron{transform:rotate(180deg)}
57
+ .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}
58
+ .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}
59
+ .dsu-option:hover,.dsu-option[data-active="true"]{background:var(--dsw-alias-bg-layer-3)}
60
+ .dsu-option:disabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}
61
+ .dsu-option:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
62
+ .dsu-optionGroup{padding:6px 10px 2px;font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary)}
63
+ .dsu-check{opacity:0;flex:none}
64
+ .dsu-option[aria-selected="true"] .dsu-check{opacity:1}
65
+ .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)}
66
+ .dsu-savebar[data-dirty="true"]{position:sticky;bottom:0;z-index:2;box-shadow:0 -12px 24px rgb(0 0 0 / 35%)}
67
+ .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}
68
+ .dsu-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l3)}
69
+ .dsu-btn:active:not(:disabled){background:var(--dsw-alias-bg-layer-1)}
70
+ .dsu-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
71
+ .dsu-btn:disabled{opacity:.4;cursor:not-allowed}
72
+ .dsu-btnPrimary{background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
73
+ .dsu-btnPrimary:hover:not(:disabled){background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);filter:brightness(.9)}
74
+ .dsu-btnPrimary:active:not(:disabled){background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);filter:brightness(.84)}
75
+ .dsu-btnDanger{color:var(--dsw-alias-state-error-primary)}
76
+ .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)}
77
+ .dsu-status{margin-left:auto;font-size:12px;color:var(--dsw-alias-label-tertiary)}
78
+ .dsu-statusError{color:var(--dsw-alias-state-error-primary)}
79
+ .dsu-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(200px,100%),1fr));gap:0 24px}
80
+ .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)}
81
+ .dsu-grid>.dsu-field .dsu-control{width:100%}
82
+ @media (max-width:640px){
83
+ .dsu-field{grid-template-columns:minmax(0,1fr);align-items:start}
84
+ .dsu-control{width:100%;max-width:720px}
85
+ .dsu-section{margin-top:20px;padding-top:20px}
86
+ }
87
+ @media (prefers-reduced-motion:reduce){
88
+ .dsu-switch,.dsu-knob,.dsu-chevron,.dsu-btn{transition:none}
89
+ }
90
+ `;
91
+ // lib/card-ui/hint.ts
92
+ /**
93
+ * 把一段完整说明拆成常显提示与浮层详情。
94
+ * 超过阈值的文案不进常显行,避免行内出现截断的省略号。
95
+ * @param text - 完整说明
96
+ * @param max - 常显阈值(字符数)
97
+ * @returns 常显提示或浮层详情
98
+ */
99
+ function splitHint(text, max = 30) {
100
+ return text.length <= max ? { hint: text } : { detail: text };
101
+ }
102
+ // lib/card-ui/layout.ts
103
+ /** 设置卡根容器:无外框、纵向排列。 */
104
+ function CardForm(props) {
105
+ return (0, react.createElement)("div", { className: "dsu-form" }, props.children);
106
+ }
107
+ /** 小节:分隔线 + 标题 + 可选说明。 */
108
+ function Section(props) {
109
+ return (0, react.createElement)("section", { className: "dsu-section" }, (0, react.createElement)("h2", { className: "dsu-sectionTitle" }, props.title), props.hint === void 0 ? null : (0, react.createElement)("p", { className: "dsu-sectionHint" }, props.hint), props.children);
110
+ }
111
+ /** 紧凑栅格:只用于工具开关这类重复布尔项。 */
112
+ function FieldGrid(props) {
113
+ return (0, react.createElement)("div", { className: "dsu-grid" }, props.children);
114
+ }
115
+ /** 一行状态 chips,以 `·` 分隔。 */
116
+ function StatusBar(props) {
117
+ if (props.items.length === 0) return null;
118
+ const children = props.items.map((item, index) => (0, react.createElement)("span", { key: index }, index === 0 ? item : `· ${item}`));
119
+ return (0, react.createElement)("div", { className: "dsu-statusbar" }, ...children);
120
+ }
121
+ // lib/card-ui/field.ts
122
+ /** 说明按钮与浮层:悬停/聚焦显示,点击钉住,Esc 关闭。 */
123
+ function InfoTip(props) {
124
+ const id = (0, react.useId)();
125
+ const [open, setOpen] = (0, react.useState)(false);
126
+ const [pinned, setPinned] = (0, react.useState)(false);
127
+ const hoveredRef = (0, react.useRef)(false);
128
+ const focusedRef = (0, react.useRef)(false);
129
+ const pinnedRef = (0, react.useRef)(false);
130
+ const anchorRef = (0, react.useRef)(null);
131
+ const tipRef = (0, react.useRef)(null);
132
+ const hideTimer = (0, react.useRef)(null);
133
+ const cancelHide = (0, react.useCallback)(() => {
134
+ if (hideTimer.current !== null) {
135
+ clearTimeout(hideTimer.current);
136
+ hideTimer.current = null;
137
+ }
138
+ }, []);
139
+ const place = (0, react.useCallback)(() => {
140
+ const anchor = anchorRef.current;
141
+ const tip = tipRef.current;
142
+ /* v8 ignore next 3 -- place 只在 open 的 layout effect 与挂载期监听器里调用;refs 为空只可能是卸载竞态 */
143
+ if (anchor === null || tip === null) return;
144
+ const rect = anchor.getBoundingClientRect();
145
+ const width = tip.offsetWidth;
146
+ const height = tip.offsetHeight;
147
+ let { left } = rect;
148
+ let top = rect.bottom + 8;
149
+ if (top + height > window.innerHeight - 8) top = rect.top - height - 8;
150
+ if (left + width > window.innerWidth - 8) left = window.innerWidth - width - 8;
151
+ const maxLeft = Math.max(8, window.innerWidth - width - 8);
152
+ const maxTop = Math.max(8, window.innerHeight - height - 8);
153
+ tip.style.left = `${Math.min(Math.max(8, left), maxLeft)}px`;
154
+ tip.style.top = `${Math.min(Math.max(8, top), maxTop)}px`;
155
+ }, []);
156
+ const close = (0, react.useCallback)(() => {
157
+ cancelHide();
158
+ pinnedRef.current = false;
159
+ setPinned(false);
160
+ setOpen(false);
161
+ }, [cancelHide]);
162
+ const show = (0, react.useCallback)(() => {
163
+ cancelHide();
164
+ setOpen(true);
165
+ }, [cancelHide]);
166
+ const scheduleHide = (0, react.useCallback)(() => {
167
+ cancelHide();
168
+ if (hoveredRef.current || focusedRef.current || pinnedRef.current) return;
169
+ hideTimer.current = setTimeout(() => {
170
+ hideTimer.current = null;
171
+ /* v8 ignore next 3 -- 任何重新进入/钉住都会 cancelHide;这次复查只防未来的异步路径 */
172
+ if (!hoveredRef.current && !focusedRef.current && !pinnedRef.current) setOpen(false);
173
+ }, 120);
174
+ }, [cancelHide]);
175
+ const updatePinned = (0, react.useCallback)((next) => {
176
+ pinnedRef.current = next;
177
+ setPinned(next);
178
+ }, []);
179
+ (0, react.useLayoutEffect)(() => {
180
+ if (!open) return () => {};
181
+ place();
182
+ const onScroll = (event) => {
183
+ const { target } = event;
184
+ if (target instanceof Node && tipRef.current?.contains(target) === true) return;
185
+ if (pinnedRef.current) {
186
+ place();
187
+ return;
188
+ }
189
+ close();
190
+ };
191
+ const onResize = () => {
192
+ place();
193
+ };
194
+ window.addEventListener("scroll", onScroll, true);
195
+ window.addEventListener("resize", onResize);
196
+ return () => {
197
+ window.removeEventListener("scroll", onScroll, true);
198
+ window.removeEventListener("resize", onResize);
199
+ };
200
+ }, [
201
+ open,
202
+ props.detail,
203
+ place,
204
+ close
205
+ ]);
206
+ (0, react.useEffect)(() => {
207
+ if (!open) return () => {};
208
+ const onKey = (event) => {
209
+ if (event.key === "Escape") close();
210
+ };
211
+ document.addEventListener("keydown", onKey);
212
+ return () => {
213
+ document.removeEventListener("keydown", onKey);
214
+ };
215
+ }, [open, close]);
216
+ (0, react.useEffect)(() => cancelHide, [cancelHide]);
217
+ const button = (0, react.createElement)("button", {
218
+ ref: anchorRef,
219
+ type: "button",
220
+ className: "dsu-helpBtn",
221
+ "aria-label": `${props.label} 说明`,
222
+ "aria-describedby": id,
223
+ "aria-pressed": pinned,
224
+ onClick: () => {
225
+ if (pinnedRef.current) {
226
+ close();
227
+ return;
228
+ }
229
+ updatePinned(true);
230
+ show();
231
+ }
232
+ }, (0, react.createElement)("svg", {
233
+ width: 12,
234
+ height: 12,
235
+ viewBox: "0 0 14 14",
236
+ "aria-hidden": true
237
+ }, (0, react.createElement)("circle", {
238
+ cx: 7,
239
+ cy: 7,
240
+ r: 6,
241
+ fill: "none",
242
+ stroke: "currentColor",
243
+ strokeWidth: 1.2
244
+ }), (0, react.createElement)("path", {
245
+ d: "M7 6.2v4",
246
+ stroke: "currentColor",
247
+ strokeWidth: 1.3,
248
+ strokeLinecap: "round"
249
+ }), (0, react.createElement)("circle", {
250
+ cx: 7,
251
+ cy: 4.3,
252
+ r: .8,
253
+ fill: "currentColor"
254
+ })));
255
+ const tip = (0, react.createElement)("div", {
256
+ ref: tipRef,
257
+ id,
258
+ role: "tooltip",
259
+ className: "dsu-tip",
260
+ hidden: !open
261
+ }, props.detail);
262
+ const labelNode = props.showLabel === false ? null : (0, react.createElement)("span", { className: `dsu-label${props.compact === true ? " dsu-labelMono" : ""}` }, props.label);
263
+ return (0, react.createElement)("span", {
264
+ className: "dsu-help",
265
+ onMouseEnter: () => {
266
+ hoveredRef.current = true;
267
+ show();
268
+ },
269
+ onMouseLeave: () => {
270
+ hoveredRef.current = false;
271
+ scheduleHide();
272
+ },
273
+ onFocus: () => {
274
+ focusedRef.current = true;
275
+ show();
276
+ },
277
+ onBlur: (event) => {
278
+ const next = event.relatedTarget;
279
+ if (next instanceof Node && event.currentTarget.contains(next)) return;
280
+ focusedRef.current = false;
281
+ scheduleHide();
282
+ }
283
+ }, labelNode, button, tip);
284
+ }
285
+ /** 字段名 + 可选说明按钮。 */
286
+ function FieldLabel(props) {
287
+ const labelClass = `dsu-label${props.compact === true ? " dsu-labelMono" : ""}`;
288
+ const labelNode = props.controlId === void 0 ? (0, react.createElement)("span", { className: labelClass }, props.label) : (0, react.createElement)("label", {
289
+ className: labelClass,
290
+ htmlFor: props.controlId
291
+ }, props.label);
292
+ return (0, react.createElement)("div", { className: "dsu-labelRow" }, labelNode, props.detail === void 0 ? null : (0, react.createElement)(InfoTip, {
293
+ label: props.label,
294
+ detail: props.detail,
295
+ showLabel: false
296
+ }));
297
+ }
298
+ /** 行式字段的基础布局:左标签/提示,右控件。 */
299
+ function Field(props) {
300
+ const hintId = props.controlId === void 0 ? void 0 : `${props.controlId}-hint`;
301
+ return (0, react.createElement)("div", { className: `dsu-field${props.wide === true ? " dsu-fieldWide" : ""}` }, (0, react.createElement)("div", { className: "dsu-fieldMeta" }, (0, react.createElement)(FieldLabel, {
302
+ label: props.label,
303
+ detail: props.detail,
304
+ compact: props.compact,
305
+ controlId: props.controlId
306
+ }), props.hint === void 0 ? null : (0, react.createElement)("p", {
307
+ className: "dsu-hint",
308
+ id: hintId
309
+ }, props.hint)), (0, react.createElement)("div", { className: "dsu-control" }, props.children));
310
+ }
311
+ // lib/card-ui/inputs.ts
312
+ /** 开关行。 */
313
+ function ToggleField(props) {
314
+ const controlId = (0, react.useId)();
315
+ const describedBy = props.hint === void 0 ? void 0 : `${controlId}-hint`;
316
+ return (0, react.createElement)(Field, {
317
+ ...props,
318
+ controlId
319
+ }, (0, react.createElement)("button", {
320
+ type: "button",
321
+ role: "switch",
322
+ id: controlId,
323
+ className: "dsu-switch",
324
+ "aria-checked": props.checked,
325
+ "aria-describedby": describedBy,
326
+ "data-field": props.field,
327
+ disabled: props.disabled === true,
328
+ onClick: () => {
329
+ props.onChange(!props.checked);
330
+ }
331
+ }, (0, react.createElement)("span", { className: "dsu-knob" })));
332
+ }
333
+ /** 数字行:以显示单位读写,越界值不写入。 */
334
+ function NumberField(props) {
335
+ const text = typeof props.value === "number" && Number.isFinite(props.value) ? String(props.value) : "";
336
+ const [draft, setDraft] = (0, react.useState)(text);
337
+ const [invalid, setInvalid] = (0, react.useState)(false);
338
+ const lastEmitted = (0, react.useRef)(void 0);
339
+ const controlId = (0, react.useId)();
340
+ const errorId = (0, react.useId)();
341
+ const describedByIds = [props.hint === void 0 ? void 0 : `${controlId}-hint`, invalid ? errorId : void 0].filter((entry) => entry !== void 0);
342
+ const describedBy = describedByIds.length === 0 ? void 0 : describedByIds.join(" ");
343
+ (0, react.useEffect)(() => {
344
+ if (props.value === lastEmitted.current) return;
345
+ lastEmitted.current = props.value;
346
+ setDraft(text);
347
+ setInvalid(false);
348
+ }, [props.value, text]);
349
+ const accepts = (value) => {
350
+ const min = props.min ?? -Infinity;
351
+ const max = props.max ?? Infinity;
352
+ if (!Number.isFinite(value) || value < min || value > max) return false;
353
+ const { step } = props;
354
+ if (step === void 0 || step <= 0) return true;
355
+ const offset = (value - (props.min ?? 0)) / step;
356
+ return Math.abs(offset - Math.round(offset)) < 1e-9;
357
+ };
358
+ const change = (next) => {
359
+ setDraft(next);
360
+ if (next === "") {
361
+ setInvalid(false);
362
+ lastEmitted.current = void 0;
363
+ props.onChange(void 0);
364
+ return;
365
+ }
366
+ const value = Number(next);
367
+ if (accepts(value)) {
368
+ setInvalid(false);
369
+ lastEmitted.current = value;
370
+ props.onChange(value);
371
+ return;
372
+ }
373
+ setInvalid(true);
374
+ };
375
+ const normalize = () => {
376
+ if (draft === "" || invalid) return;
377
+ setDraft(String(Number(draft)));
378
+ };
379
+ return (0, react.createElement)(Field, {
380
+ ...props,
381
+ controlId
382
+ }, (0, react.createElement)("div", { className: "dsu-numberWrap" }, (0, react.createElement)("div", { className: "dsu-number" }, (0, react.createElement)("input", {
383
+ className: "dsu-input",
384
+ type: "number",
385
+ inputMode: "numeric",
386
+ id: controlId,
387
+ value: draft,
388
+ min: props.min,
389
+ max: props.max,
390
+ step: props.step,
391
+ placeholder: props.placeholder,
392
+ "aria-invalid": invalid || void 0,
393
+ "aria-describedby": describedBy,
394
+ "data-field": props.field,
395
+ disabled: props.disabled === true,
396
+ onChange: (event) => {
397
+ change(event.target.value);
398
+ },
399
+ onBlur: normalize
400
+ }), props.unit === void 0 ? null : (0, react.createElement)("span", { className: "dsu-unit" }, props.unit)), invalid ? (0, react.createElement)("p", {
401
+ className: "dsu-error",
402
+ id: errorId
403
+ }, props.invalidHint) : null));
404
+ }
405
+ /** 单行/多行文本。 */
406
+ function TextField(props) {
407
+ const [draft, setDraft] = (0, react.useState)(props.value);
408
+ const [focused, setFocused] = (0, react.useState)(false);
409
+ const controlId = (0, react.useId)();
410
+ const describedBy = props.hint === void 0 ? void 0 : `${controlId}-hint`;
411
+ (0, react.useEffect)(() => {
412
+ if (!focused) setDraft(props.value);
413
+ }, [props.value, focused]);
414
+ const shared = {
415
+ id: controlId,
416
+ className: `dsu-input${props.multiline === true ? " dsu-textarea" : ""}${props.monospace === true ? " dsu-mono" : ""}`,
417
+ value: draft,
418
+ placeholder: props.placeholder,
419
+ "aria-describedby": describedBy,
420
+ "data-field": props.field,
421
+ disabled: props.disabled === true,
422
+ onFocus: () => {
423
+ setFocused(true);
424
+ },
425
+ onBlur: () => {
426
+ setFocused(false);
427
+ },
428
+ onChange: (event) => {
429
+ setDraft(event.target.value);
430
+ props.onChange(event.target.value);
431
+ }
432
+ };
433
+ return (0, react.createElement)(Field, {
434
+ ...props,
435
+ controlId
436
+ }, props.multiline === true ? (0, react.createElement)("textarea", shared) : (0, react.createElement)("input", shared));
437
+ }
438
+ /** 名单行:逗号或换行分隔。逗号形态不支持条目本身含逗号;自由文本与正则用 newline。 */
439
+ function ListField(props) {
440
+ const separator = props.separator ?? "comma";
441
+ const joined = props.value.join(separator === "newline" ? "\n" : ", ");
442
+ const [draft, setDraft] = (0, react.useState)(joined);
443
+ const [focused, setFocused] = (0, react.useState)(false);
444
+ const controlId = (0, react.useId)();
445
+ const describedBy = props.hint === void 0 ? void 0 : `${controlId}-hint`;
446
+ (0, react.useEffect)(() => {
447
+ if (!focused) setDraft(joined);
448
+ }, [joined, focused]);
449
+ const split = (raw) => raw.split(separator === "newline" ? "\n" : ",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
450
+ const shared = {
451
+ id: controlId,
452
+ className: `dsu-input${separator === "newline" ? " dsu-textarea dsu-mono" : ""}`,
453
+ value: draft,
454
+ placeholder: props.placeholder,
455
+ "aria-describedby": describedBy,
456
+ "data-field": props.field,
457
+ disabled: props.disabled === true,
458
+ onFocus: () => {
459
+ setFocused(true);
460
+ },
461
+ onBlur: () => {
462
+ setFocused(false);
463
+ },
464
+ onChange: (event) => {
465
+ setDraft(event.target.value);
466
+ props.onChange(split(event.target.value));
467
+ }
468
+ };
469
+ return (0, react.createElement)(Field, {
470
+ ...props,
471
+ controlId
472
+ }, separator === "newline" ? (0, react.createElement)("textarea", shared) : (0, react.createElement)("input", shared));
473
+ }
474
+ // lib/card-ui/select.ts
475
+ /** 自定义下拉:浮层固定定位,键盘可选,点击外部关闭。 */
476
+ function SelectField(props) {
477
+ const [open, setOpen] = (0, react.useState)(false);
478
+ const [active, setActive] = (0, react.useState)(0);
479
+ const triggerRef = (0, react.useRef)(null);
480
+ const menuRef = (0, react.useRef)(null);
481
+ const baseId = (0, react.useId)();
482
+ const controlId = (0, react.useId)();
483
+ const describedBy = props.hint === void 0 ? void 0 : `${controlId}-hint`;
484
+ const { disabled, value, options, onChange } = props;
485
+ const enabled = (0, react.useMemo)(() => options.map((option, index) => ({
486
+ option,
487
+ index
488
+ })).filter((entry) => entry.option.disabled !== true), [options]);
489
+ const selectedLabel = options.find((option) => option.value === value)?.label ?? value;
490
+ const activeOption = options[active];
491
+ const activeValid = activeOption !== void 0 && activeOption.disabled !== true;
492
+ const place = (0, react.useCallback)(() => {
493
+ const trigger = triggerRef.current;
494
+ const menu = menuRef.current;
495
+ /* v8 ignore next 3 -- place 只在 open 的 layout effect 里调用;refs 为空只可能是卸载竞态 */
496
+ if (trigger === null || menu === null) return;
497
+ const rect = trigger.getBoundingClientRect();
498
+ const width = Math.min(rect.width, window.innerWidth - 16);
499
+ let { left } = rect;
500
+ if (left + width > window.innerWidth - 8) left = window.innerWidth - width - 8;
501
+ const height = menu.offsetHeight;
502
+ let top = rect.bottom + 6;
503
+ if (top + height > window.innerHeight - 8) top = rect.top - height - 6;
504
+ const maxLeft = Math.max(8, window.innerWidth - width - 8);
505
+ const maxTop = Math.max(8, window.innerHeight - height - 8);
506
+ menu.style.width = `${width}px`;
507
+ menu.style.left = `${Math.min(Math.max(8, left), maxLeft)}px`;
508
+ menu.style.top = `${Math.min(Math.max(8, top), maxTop)}px`;
509
+ }, []);
510
+ const close = (0, react.useCallback)(() => {
511
+ setOpen(false);
512
+ }, []);
513
+ const openMenu = (0, react.useCallback)(() => {
514
+ if (disabled === true) return;
515
+ const initial = enabled.find((entry) => entry.option.value === value) ?? enabled.at(0);
516
+ if (initial === void 0) return;
517
+ setActive(initial.index);
518
+ setOpen(true);
519
+ }, [
520
+ disabled,
521
+ value,
522
+ enabled
523
+ ]);
524
+ const select = (0, react.useCallback)((index) => {
525
+ /* v8 ignore next 3 -- 浮层只能经由 openMenu 打开;组件禁用时打不开 */
526
+ if (disabled === true) return;
527
+ const option = options[index];
528
+ /* v8 ignore next 3 -- disabled 选项在浏览器里不派发 click;越界下标由调用面保证 */
529
+ if (option === void 0 || option.disabled === true) return;
530
+ onChange(option.value);
531
+ close();
532
+ triggerRef.current?.focus();
533
+ }, [
534
+ disabled,
535
+ options,
536
+ onChange,
537
+ close
538
+ ]);
539
+ const move = (0, react.useCallback)((delta) => {
540
+ const position = enabled.findIndex((entry) => entry.index === active);
541
+ /* v8 ignore next 5 -- open 时归一 effect 已保证 enabled 非空且 active 落在可用项上 */
542
+ if (position === -1 || enabled.length === 0) return;
543
+ const next = (position + delta + enabled.length) % enabled.length;
544
+ const entry = enabled[next];
545
+ /* v8 ignore next 3 -- next 由取模保证在界内 */
546
+ if (entry === void 0) return;
547
+ setActive(entry.index);
548
+ }, [active, enabled]);
549
+ (0, react.useLayoutEffect)(() => {
550
+ if (!open) return () => {};
551
+ place();
552
+ const onPointerDown = (event) => {
553
+ const target = event.target;
554
+ if (menuRef.current?.contains(target) === true) return;
555
+ if (triggerRef.current?.contains(target) === true) return;
556
+ close();
557
+ };
558
+ const onScroll = (event) => {
559
+ const { target } = event;
560
+ if (target instanceof Node && menuRef.current?.contains(target) === true) return;
561
+ close();
562
+ };
563
+ const onResize = () => {
564
+ place();
565
+ };
566
+ document.addEventListener("pointerdown", onPointerDown, true);
567
+ window.addEventListener("scroll", onScroll, true);
568
+ window.addEventListener("resize", onResize);
569
+ return () => {
570
+ document.removeEventListener("pointerdown", onPointerDown, true);
571
+ window.removeEventListener("scroll", onScroll, true);
572
+ window.removeEventListener("resize", onResize);
573
+ };
574
+ }, [
575
+ open,
576
+ props.options,
577
+ place,
578
+ close
579
+ ]);
580
+ (0, react.useEffect)(() => {
581
+ if (!open) return;
582
+ if (disabled === true || enabled.length === 0) {
583
+ close();
584
+ return;
585
+ }
586
+ if (enabled.some((entry) => entry.index === active)) return;
587
+ const fallback = enabled.find((entry) => entry.option.value === value) ?? enabled.at(0);
588
+ /* v8 ignore next 3 -- enabled 非空在上一分支已保证 */
589
+ if (fallback === void 0) return;
590
+ setActive(fallback.index);
591
+ }, [
592
+ open,
593
+ disabled,
594
+ value,
595
+ options,
596
+ active,
597
+ close,
598
+ enabled
599
+ ]);
600
+ (0, react.useLayoutEffect)(() => {
601
+ if (!open) return;
602
+ const menu = menuRef.current;
603
+ /* v8 ignore next 2 -- 同上,menu 在 open 的 commit 之后必然已挂上 */
604
+ if (menu === null) return;
605
+ const node = menu.querySelector("[data-active=\"true\"]");
606
+ /* v8 ignore next 2 -- active 已由归一 effect 保证落在可用项上 */
607
+ if (node === null) return;
608
+ if (node.offsetTop < menu.scrollTop) {
609
+ menu.scrollTop = node.offsetTop;
610
+ return;
611
+ }
612
+ if (node.offsetTop + node.offsetHeight > menu.scrollTop + menu.clientHeight) menu.scrollTop = node.offsetTop + node.offsetHeight - menu.clientHeight;
613
+ }, [
614
+ open,
615
+ active,
616
+ options
617
+ ]);
618
+ const onKeyDown = (event) => {
619
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
620
+ event.preventDefault();
621
+ if (!open) {
622
+ openMenu();
623
+ return;
624
+ }
625
+ move(event.key === "ArrowDown" ? 1 : -1);
626
+ return;
627
+ }
628
+ if (event.key === "Home" || event.key === "End") {
629
+ event.preventDefault();
630
+ if (!open) openMenu();
631
+ if (enabled.length === 0) return;
632
+ const entry = event.key === "Home" ? enabled.at(0) : enabled.at(-1);
633
+ /* v8 ignore next 3 -- enabled 为空已在上一条返回 */
634
+ if (entry === void 0) return;
635
+ setActive(entry.index);
636
+ return;
637
+ }
638
+ if (event.key === "Enter" || event.key === " ") {
639
+ event.preventDefault();
640
+ if (!open) {
641
+ openMenu();
642
+ return;
643
+ }
644
+ select(active);
645
+ return;
646
+ }
647
+ if (event.key === "Tab") {
648
+ close();
649
+ return;
650
+ }
651
+ if (event.key === "Escape") close();
652
+ };
653
+ const optionNodes = [];
654
+ let lastGroup;
655
+ for (const [index, option] of options.entries()) {
656
+ if (option.group !== lastGroup) {
657
+ if (option.group !== void 0) optionNodes.push((0, react.createElement)("div", {
658
+ key: `group-${index}-${option.group}`,
659
+ className: "dsu-optionGroup",
660
+ role: "presentation"
661
+ }, option.group));
662
+ lastGroup = option.group;
663
+ }
664
+ optionNodes.push((0, react.createElement)("button", {
665
+ key: `option-${option.value}`,
666
+ type: "button",
667
+ role: "option",
668
+ id: `${baseId}-${index}`,
669
+ className: "dsu-option",
670
+ tabIndex: -1,
671
+ "aria-selected": option.value === props.value,
672
+ "data-active": index === active,
673
+ disabled: option.disabled === true,
674
+ onMouseDown: (event) => {
675
+ event.preventDefault();
676
+ },
677
+ onClick: () => {
678
+ select(index);
679
+ },
680
+ onMouseEnter: () => {
681
+ /* v8 ignore next -- happy-dom 对 disabled 元素不派发 mouse 事件;浏览器实现各有差异,保留禁用检查 */
682
+ if (option.disabled !== true) setActive(index);
683
+ }
684
+ }, (0, react.createElement)("span", null, option.label), (0, react.createElement)("svg", {
685
+ className: "dsu-check",
686
+ width: 14,
687
+ height: 14,
688
+ viewBox: "0 0 14 14",
689
+ "aria-hidden": true
690
+ }, (0, react.createElement)("path", {
691
+ d: "M2.5 7.5l3 3 6-7",
692
+ fill: "none",
693
+ stroke: "currentColor",
694
+ strokeWidth: 1.5,
695
+ strokeLinecap: "round",
696
+ strokeLinejoin: "round"
697
+ }))));
698
+ }
699
+ return (0, react.createElement)(Field, {
700
+ ...props,
701
+ controlId
702
+ }, (0, react.createElement)("div", {
703
+ className: "dsu-selectWrap",
704
+ onBlur: (event) => {
705
+ const next = event.relatedTarget;
706
+ if (next instanceof Node && event.currentTarget.contains(next)) return;
707
+ close();
708
+ }
709
+ }, (0, react.createElement)("button", {
710
+ ref: triggerRef,
711
+ type: "button",
712
+ id: controlId,
713
+ className: "dsu-select dsu-input",
714
+ role: "combobox",
715
+ "aria-haspopup": "listbox",
716
+ "aria-expanded": open,
717
+ "aria-controls": `${baseId}-list`,
718
+ "aria-activedescendant": open && activeValid ? `${baseId}-${active}` : void 0,
719
+ "aria-describedby": describedBy,
720
+ "data-field": props.field,
721
+ disabled: disabled === true || options.length === 0,
722
+ onClick: () => {
723
+ if (open) {
724
+ close();
725
+ return;
726
+ }
727
+ openMenu();
728
+ },
729
+ onKeyDown
730
+ }, (0, react.createElement)("span", { className: "dsu-selectValue" }, selectedLabel), (0, react.createElement)("svg", {
731
+ className: "dsu-chevron",
732
+ width: 14,
733
+ height: 14,
734
+ viewBox: "0 0 14 14",
735
+ "aria-hidden": true
736
+ }, (0, react.createElement)("path", {
737
+ d: "M3 5l4 4 4-4",
738
+ fill: "none",
739
+ stroke: "currentColor",
740
+ strokeWidth: 1.5,
741
+ strokeLinecap: "round",
742
+ strokeLinejoin: "round"
743
+ }))), (0, react.createElement)("div", {
744
+ ref: menuRef,
745
+ id: `${baseId}-list`,
746
+ role: "listbox",
747
+ className: "dsu-menu",
748
+ "aria-label": props.label,
749
+ hidden: !open
750
+ }, ...optionNodes)));
751
+ }
752
+ // lib/card-ui/save-bar.ts
753
+ /** 底部保存条:dirty 时吸底,显示条数与失败原因。 */
754
+ function SaveBar(props) {
755
+ const disabled = !props.writable || props.busy || !props.dirty;
756
+ let status;
757
+ if (props.error !== null) status = props.error;
758
+ else if (props.writable) status = props.dirty ? props.dirtyLabel : props.cleanLabel;
759
+ else status = props.readOnlyLabel;
760
+ return (0, react.createElement)("div", {
761
+ className: "dsu-savebar",
762
+ "data-dirty": props.dirty
763
+ }, (0, react.createElement)("button", {
764
+ type: "button",
765
+ className: "dsu-btn dsu-btnPrimary",
766
+ "data-field": "save",
767
+ disabled,
768
+ onClick: props.onSave
769
+ }, props.busy ? props.savingLabel : props.saveLabel), (0, react.createElement)("button", {
770
+ type: "button",
771
+ className: "dsu-btn",
772
+ "data-field": "discard",
773
+ disabled,
774
+ onClick: props.onDiscard
775
+ }, props.revertLabel), (0, react.createElement)("span", { className: props.error === null ? "dsu-status" : "dsu-status dsu-statusError" }, status));
776
+ }
777
+ // lib/card-diff.ts
778
+ /**
779
+ * 两个值按「表单语义」是否相等:null、undefined 与缺失等价;对象比键集与每个值,
780
+ * 数组比长度与每个元素;其余走 `Object.is`(于是 NaN 与 NaN 相等,`-0` 与 `0` 不等)。
781
+ *
782
+ * 不用 `JSON.stringify` 比较:它把 `NaN`/`Infinity` 都渲染成 `"null"`(`{a:NaN}` 与
783
+ * `{a:null}` 会被判成没改),把 `BigInt` 与循环引用直接抛 `TypeError`,而对象的键序
784
+ * 又会把「同内容的两个对象」判成改动——三个方向都错,且错得不报错。
785
+ * @param left - 一侧的值
786
+ * @param right - 另一侧的值
787
+ * @param seen - 已访问过的「左 → 右」配对,用于在循环引用上正常结束
788
+ * @returns 是否相等
789
+ */
790
+ function formValueEquals(left, right, seen) {
791
+ if (left === null || left === void 0 || right === null || right === void 0) return (left ?? null) === (right ?? null);
792
+ if (Object.is(left, right)) return true;
793
+ if (typeof left !== "object" || typeof right !== "object") return false;
794
+ if (seen.get(left) === right) return true;
795
+ seen.set(left, right);
796
+ if (Array.isArray(left) || Array.isArray(right)) {
797
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
798
+ return left.every((item, index) => formValueEquals(item, right[index], seen));
799
+ }
800
+ const leftKeys = Object.keys(left);
801
+ const rightKeys = Object.keys(right);
802
+ if (leftKeys.length !== rightKeys.length) return false;
803
+ return leftKeys.every((key) => Object.hasOwn(right, key) && formValueEquals(Reflect.get(left, key), Reflect.get(right, key), seen));
804
+ }
805
+ /**
806
+ * touched 层与快照的差异字段(值语义比较;undefined、null 与缺失三者等价)。
807
+ *
808
+ * 比较用 {@link formValueEquals} 而不是 `===` 或 `JSON.stringify`:引用相等会把「用户重排了
809
+ * 同内容的数组」判成改动,而序列化比较会在 NaN、键序、BigInt 与循环引用上给出错误答案。
810
+ * @param touched 草稿层里被碰过的字段
811
+ * @param value 官方快照里的当前值
812
+ * @returns 真正发生改动的字段名,顺序随 `touched` 的键序
813
+ */
814
+ function diffTouched(touched, value) {
815
+ const out = [];
816
+ for (const key of Object.keys(touched)) if (!formValueEquals(touched[key], value[key], /* @__PURE__ */ new WeakMap())) out.push(key);
817
+ return out;
818
+ }
819
+ // lib/card-apply.ts
820
+ /**
821
+ * flag 的合法形态:非空、且不得命中 `Object.prototype` 的成员。`__proto__` 写不进自有属性
822
+ * (setter 忽略非对象值),函数却返回 true,守卫从此永远失灵;`__defineGetter__` 一类会在
823
+ * globalThis 上落下自有属性、遮蔽同名原型方法——装载即抛。`__` 前缀是各消费卡的书写
824
+ * 惯例,不是判断依据:判断只拦真正会破坏保护逻辑或全局对象的键。
825
+ */
826
+ const isLegalClaimFlag = (flag) => flag.length > 0 && !(flag in Object.prototype);
827
+ /**
828
+ * 把占位槽恢复到 claim 之前的状态。此前没有自有属性时置 `undefined` 而非 delete:
829
+ * 删动态键会被本仓 lint 基线的 typescript/no-dynamic-delete 判定失败,它援引的正是
830
+ * 「频繁 delete 会把对象推进 V8 字典模式、内联缓存失效」那一条——判断读的是
831
+ * 「是否仍被占位」,残留的 undefined 键不影响保护逻辑。
832
+ */
833
+ function restoreClaimSlot(marker, flag, previous) {
834
+ if (previous === void 0) {
835
+ marker[flag] = void 0;
836
+ return;
837
+ }
838
+ Object.defineProperty(marker, flag, previous);
839
+ }
840
+ /**
841
+ * apply 幂等保护:已 claim 返回 false(调用方直接 return),否则占位并
842
+ * 挂 ctx.effect 清理——HMR 重载时 flag 复位,再次 apply 可重新声明。
843
+ *
844
+ * 占位值是本次 claim 现造的 symbol;保护对外的判断标准是「自有键上的 `true` 或任何 symbol」:
845
+ * `true` 是外部可写的占位约定(判断放宽一分就重新打开 HMR 重复装载),symbol 标识让
846
+ * 重复加载的另一份模块实例的占位同样被认出——两份实例各持各的 token,靠 typeof 而不是
847
+ * 身份对上。清理只认**自己那个** token:旧实例的 disposer 迟于新实例 claim 执行(HMR /
848
+ * 重复加载不保证时序,也不保证恰好调一次)时,它认不得新实例的占位,新 claim 得以保留;
849
+ * claim 之前槽上已有的自有值按原样恢复,不抹成 undefined。
850
+ *
851
+ * ⚠ 标记是**进程级**的(同一个 flag 在全页面只有一份),不是 per-ctx:同一张卡被第二个
852
+ * ctx apply 时会被这一个拒绝。要允许多 ctx 共存就得按 ctx 管理(WeakSet 一类),
853
+ * 别改成这里"顺手放过第二次"——那会重新打开 HMR 重复装载这条路。
854
+ * @param ctx - apply 的 ctx(需要 effect)。
855
+ * @param flag - globalThis 上的占位标记键名(如 "__xxxCardApplied"),形态见 {@link isLegalClaimFlag}。
856
+ * @param label - effect 标签(日志/生命周期可观测)。
857
+ * @returns true 表示本次拿到声明权(继续执行),false 表示已被占位。
858
+ */
859
+ function claimApply(ctx, flag, label) {
860
+ if (!isLegalClaimFlag(flag)) throw new Error(`claimApply 的 flag 不得为空、也不得命中 Object.prototype 的成员(__proto__ 一类会让占位静默失灵或遮蔽全局成员):${flag}`);
861
+ const marker = globalThis;
862
+ const current = Object.hasOwn(marker, flag) ? marker[flag] : void 0;
863
+ if (current === true || typeof current === "symbol") return false;
864
+ const previous = Object.getOwnPropertyDescriptor(marker, flag);
865
+ const token = Symbol(flag);
866
+ marker[flag] = token;
867
+ try {
868
+ ctx.effect(() => () => {
869
+ if (marker[flag] === token) restoreClaimSlot(marker, flag, previous);
870
+ }, label);
871
+ } catch (error) {
872
+ if (marker[flag] === token) restoreClaimSlot(marker, flag, previous);
873
+ throw error;
874
+ }
875
+ return true;
876
+ }
877
+ // lib/record.ts
878
+ /** 对象类型守卫:非 `null` 的 `object` 且非数组(`Record` 索引签名投影的基础)。 */
879
+ function isRecord(value) {
880
+ return typeof value === "object" && value !== null && !Array.isArray(value);
881
+ }
882
+ /** 读取抛错(恶意 getter / Proxy 的 get trap)时的共同结局:按「该成员取不到」处理。 */
883
+ const ABSENT_READ = { absent: true };
884
+ /**
885
+ * 带保护的成员读取。被测对象来自 `ctx.get(...)` 的 unknown,可能带 getter 或 Proxy trap:
886
+ * 读取抛错在出口处变成「取不到」而不是向外冒泡——本文件的两个出口都是能力检查,
887
+ * 一次抛错会让 `typeof fieldOf(x, "describe") === "function"` 这类表达式整体抛错,
888
+ * 把「能力不存在」放大成流程崩溃。
889
+ */
890
+ function readMember(value, key) {
891
+ try {
892
+ return {
893
+ absent: false,
894
+ value: value[key]
895
+ };
896
+ } catch {
897
+ return ABSENT_READ;
898
+ }
899
+ }
900
+ /**
901
+ * 从 `unknown` 投影单个字段;非 `Record` → `undefined`。
902
+ *
903
+ * ⚠ **取值走原型链,这是有意为之,不是疏忽**:`value[key]` 不做 `hasOwn` 判定,所以原型上的
904
+ * 方法照样取得到。消费方正靠这一条做**能力检查**——cordis 服务对象的方法挂在原型上、不是
905
+ * 自有键,形如 `typeof fieldOf(settings, "describe") === "function"`、
906
+ * `hasMethods(fieldOf(value, "timer"), ["timeout"])`、`typeof fieldOf(session,
907
+ * "snapshotEvents") === "function"`(消费包的调用点记在 test/record.test.ts)。
908
+ * 改成 `Object.hasOwn` 会让上面几处**一律判否**,那些包随即认不出合法的 cordis 服务——
909
+ * 而这类误判的表现是「能力检查静默返回 false」,不是报错,极难往这里归因。
910
+ *
911
+ * 代价是明写在这里:键名取到的是原型成员时(`fieldOf({}, "__proto__")` 交回
912
+ * `Object.prototype`、`fieldOf({}, "constructor")` 交回 `Object`)。这是本函数的取舍,
913
+ * 不是让消费方猜的属性——现存调用点的键名全是字面量,没有一处来自数据。
914
+ * test/record.test.ts 两侧都有用例固定:原型方法取得到(这是关键行为),自有键优先于原型键。
915
+ *
916
+ * ⚠ 本段刻意不写出兄弟包的名字:**声明上的 JSDoc 会被 tsc 抄进 dist/types/record.d.ts 一起
917
+ * 发出去**,包名会让装了这个包的消费方以为存在一个并不存在的依赖
918
+ *(test/publish-manifest.test.ts 的产物检查会判定失败)。要写具体调用点就写在 test/ 里。
919
+ *
920
+ * 读取走的是带保护的出口:被测对象带抛错 getter / Proxy trap 时按「取不到」交回
921
+ * `undefined`,异常不冒泡(能力检查不该打断流程)。
922
+ */
923
+ function fieldOf(value, key) {
924
+ const read = isRecord(value) ? readMember(value, key) : ABSENT_READ;
925
+ return read.absent ? void 0 : read.value;
926
+ }
927
+ // lib/errors.ts
928
+ /** 取对象**自有**的 message 属性;不是可读字符串则回 undefined。 */
929
+ function ownMessage(value) {
930
+ const descriptor = Object.getOwnPropertyDescriptor(value, "message");
931
+ return descriptor !== void 0 && "value" in descriptor && typeof descriptor.value === "string" ? descriptor.value : void 0;
932
+ }
933
+ /** 渲染一个值,沿 cause 链递归;环上回访改用占位文本。 */
934
+ function render(value, path) {
935
+ if (path.has(value)) return "<circular cause>";
936
+ path.add(value);
937
+ try {
938
+ if (!(value instanceof Error)) {
939
+ if (typeof value === "object" && value !== null) {
940
+ const owned = ownMessage(value);
941
+ if (owned !== void 0 && owned !== "") return owned;
942
+ }
943
+ return String(value);
944
+ }
945
+ const message = value.message === "" ? value.name : value.message;
946
+ const members = typeof globalThis.AggregateError === "function" && value instanceof AggregateError && value.errors.length > 0 ? ` [${value.errors.map((member) => render(member, path)).join("; ")}]` : "";
947
+ const causeText = value.cause === void 0 || value.cause === null ? "" : render(value.cause, path);
948
+ return `${message}${members}${causeText === "" || causeText === message ? "" : `: ${causeText}`}`;
949
+ } catch {
950
+ return "<unrenderable value>";
951
+ } finally {
952
+ path.delete(value);
953
+ }
954
+ }
955
+ /**
956
+ * 把捕获到的值渲染成一段人读文本。
957
+ * `Error` 取 message(空串时回退 name)并接上 cause 链;`AggregateError` 附上成员;
958
+ * 非 Error 但带可读 message 属性的取该属性,其余按 `String()` 处理。
959
+ * 全程不抛异常:恶意输入改用占位文本。
960
+ * @param error - 捕获到的值(catch 里的 `unknown`)。
961
+ * @returns 可直接进界面通知或日志的文本。
962
+ */
963
+ function errorText(error) {
964
+ return render(error, /* @__PURE__ */ new Set());
965
+ }
966
+ // lib/client-entry.ts
967
+ const name = "@jayyuen66/dsh-plugin-shared";
968
+ /** 客户端行只提供组件与工具函数;这个入口只让 Cordis 客户端把行当作插件装载。 */
969
+ function apply() {}
970
+ exports.CARD_UI_CSS = CARD_UI_CSS;
971
+ exports.CardForm = CardForm;
972
+ exports.Field = Field;
973
+ exports.FieldGrid = FieldGrid;
974
+ exports.FieldLabel = FieldLabel;
975
+ exports.InfoTip = InfoTip;
976
+ exports.ListField = ListField;
977
+ exports.NumberField = NumberField;
978
+ exports.SaveBar = SaveBar;
979
+ exports.Section = Section;
980
+ exports.SelectField = SelectField;
981
+ exports.StatusBar = StatusBar;
982
+ exports.TextField = TextField;
983
+ exports.ToggleField = ToggleField;
984
+ exports.apply = apply;
985
+ exports.claimApply = claimApply;
986
+ exports.diffTouched = diffTouched;
987
+ exports.errorText = errorText;
988
+ exports.fieldOf = fieldOf;
989
+ exports.isRecord = isRecord;
990
+ exports.name = name;
991
+ exports.splitHint = splitHint;
992
+ return module.exports;
993
+ },
994
+ });