@dsh-plus/notify-email 0.1.9 → 0.1.11
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/lib/client.js +523 -386
- package/package.json +3 -2
- package/src/client/card.tsx +175 -187
- package/src/client/client.ts +6 -86
- package/src/client/i18n.ts +16 -33
- package/src/client/fields.tsx +0 -75
- package/src/client/scope.ts +0 -59
- package/src/client/styles.ts +0 -64
package/lib/client.js
CHANGED
|
@@ -4,53 +4,113 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
let react = require("react");
|
|
8
7
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
let react = require("react");
|
|
9
|
+
//#region ../shared/src/client/icons.tsx
|
|
10
|
+
/** 官方 IconChevronDownOutline14 的同构实现(path 数据逐字一致)。 */
|
|
11
|
+
function ChevronDownIcon(props) {
|
|
12
|
+
const size = props.size ?? 14;
|
|
13
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
14
|
+
width: size,
|
|
15
|
+
height: size,
|
|
16
|
+
className: props.className,
|
|
17
|
+
viewBox: "0 0 14 14",
|
|
18
|
+
fill: "none",
|
|
19
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
20
|
+
"aria-hidden": "true",
|
|
21
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
22
|
+
d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
|
|
23
|
+
fill: "currentColor"
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region ../shared/src/client/card.tsx
|
|
29
|
+
const IDLE_STATUS = {
|
|
30
|
+
kind: "idle",
|
|
31
|
+
text: ""
|
|
32
|
+
};
|
|
33
|
+
function CardChrome(props) {
|
|
34
|
+
const p = props.prefix;
|
|
35
|
+
const badge = props.statusBadge;
|
|
36
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
37
|
+
className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
|
|
38
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
39
|
+
type: "button",
|
|
40
|
+
className: `${p}-header`,
|
|
41
|
+
"aria-expanded": props.open,
|
|
42
|
+
"aria-label": props.title,
|
|
43
|
+
onClick: () => props.onToggle(!props.open),
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
46
|
+
className: `${p}-headText`,
|
|
47
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
48
|
+
className: `${p}-name`,
|
|
49
|
+
children: props.title
|
|
50
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
51
|
+
className: `${p}-description`,
|
|
52
|
+
children: props.description
|
|
53
|
+
})]
|
|
54
|
+
}),
|
|
55
|
+
badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
56
|
+
className: `${p}-statusBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
|
|
57
|
+
children: badge.text
|
|
58
|
+
}) : null,
|
|
59
|
+
props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
60
|
+
className: `${p}-pending`,
|
|
61
|
+
children: props.dirtyLabel
|
|
62
|
+
}) : null,
|
|
63
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
|
|
64
|
+
]
|
|
65
|
+
}), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
66
|
+
className: `${p}-body`,
|
|
67
|
+
children: [
|
|
68
|
+
props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
69
|
+
className: `${p}-readOnly`,
|
|
70
|
+
role: "status",
|
|
71
|
+
children: props.readOnlyNotice
|
|
72
|
+
}) : null,
|
|
73
|
+
props.children,
|
|
74
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
75
|
+
className: `${p}-footer`,
|
|
76
|
+
children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
77
|
+
className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
|
|
78
|
+
role: "status",
|
|
79
|
+
children: props.status.text
|
|
80
|
+
}) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
81
|
+
type: "button",
|
|
82
|
+
className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
|
|
83
|
+
disabled: action.disabled === true,
|
|
84
|
+
onClick: action.onClick,
|
|
85
|
+
children: action.label
|
|
86
|
+
}, action.key))]
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
}) : null]
|
|
90
|
+
});
|
|
32
91
|
}
|
|
33
92
|
//#endregion
|
|
34
|
-
//#region src/client/fields.tsx
|
|
93
|
+
//#region ../shared/src/client/fields.tsx
|
|
35
94
|
function TextField(props) {
|
|
95
|
+
const p = props.prefix ?? "dshp";
|
|
36
96
|
const invalid = props.invalid === true;
|
|
37
97
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
38
|
-
className:
|
|
98
|
+
className: `${p}-field`,
|
|
39
99
|
children: [
|
|
40
100
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
41
|
-
className:
|
|
101
|
+
className: `${p}-head`,
|
|
42
102
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
43
|
-
className:
|
|
103
|
+
className: `${p}-label`,
|
|
44
104
|
htmlFor: props.id,
|
|
45
105
|
children: props.label
|
|
46
106
|
}), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
47
|
-
className:
|
|
107
|
+
className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
|
|
48
108
|
children: props.badge.text
|
|
49
109
|
}) : null]
|
|
50
110
|
}),
|
|
51
111
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
52
112
|
id: props.id,
|
|
53
|
-
className:
|
|
113
|
+
className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
|
|
54
114
|
type: props.password === true ? "password" : "text",
|
|
55
115
|
inputMode: props.numeric === true ? "numeric" : void 0,
|
|
56
116
|
autoComplete: props.password === true ? "off" : void 0,
|
|
@@ -60,18 +120,26 @@ window.__ModuleLoader__.load({
|
|
|
60
120
|
onChange: (event) => props.onEdit(event.target.value)
|
|
61
121
|
}),
|
|
62
122
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
63
|
-
className: invalid ?
|
|
123
|
+
className: invalid ? `${p}-invalid` : `${p}-hint`,
|
|
64
124
|
children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
|
|
65
125
|
})
|
|
66
126
|
]
|
|
67
127
|
});
|
|
68
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
|
|
131
|
+
* 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
|
|
132
|
+
* 语义保持 checkbox(无障碍树与表单语义不变),视觉自绘。
|
|
133
|
+
*/
|
|
69
134
|
function CheckRow(props) {
|
|
135
|
+
const p = props.prefix ?? "dshp";
|
|
70
136
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
71
|
-
className:
|
|
137
|
+
className: `${p}-checkRow`,
|
|
72
138
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
73
139
|
id: props.id,
|
|
74
140
|
type: "checkbox",
|
|
141
|
+
role: "switch",
|
|
142
|
+
"aria-checked": props.checked,
|
|
75
143
|
checked: props.checked,
|
|
76
144
|
disabled: props.disabled === true,
|
|
77
145
|
onChange: (event) => props.onEdit(event.target.checked)
|
|
@@ -82,20 +150,258 @@ window.__ModuleLoader__.load({
|
|
|
82
150
|
});
|
|
83
151
|
}
|
|
84
152
|
//#endregion
|
|
153
|
+
//#region ../shared/src/client/i18n.ts
|
|
154
|
+
/** 卡片通用文案(save/discard/unsaved/loading 等四插件共有键)。 */
|
|
155
|
+
const commonZh = {
|
|
156
|
+
save: "保存",
|
|
157
|
+
saving: "保存中…",
|
|
158
|
+
discard: "放弃",
|
|
159
|
+
unsaved: "未保存",
|
|
160
|
+
loading: "加载中…",
|
|
161
|
+
unavailable: "配置服务不可用。",
|
|
162
|
+
readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
|
|
163
|
+
invalidNumber: "请输入正整数。",
|
|
164
|
+
collapse: "收起",
|
|
165
|
+
expand: "展开",
|
|
166
|
+
enabledOn: "已启用",
|
|
167
|
+
enabledOff: "未启用",
|
|
168
|
+
saveFailed: "保存失败,请检查填写内容。",
|
|
169
|
+
confirm: "确认",
|
|
170
|
+
cancel: "取消"
|
|
171
|
+
};
|
|
172
|
+
const commonEn = {
|
|
173
|
+
save: "Save",
|
|
174
|
+
saving: "Saving…",
|
|
175
|
+
discard: "Discard",
|
|
176
|
+
unsaved: "Unsaved",
|
|
177
|
+
loading: "Loading…",
|
|
178
|
+
unavailable: "Settings service unavailable.",
|
|
179
|
+
readOnly: "No settings provider in this deployment; config is read-only. Edit settings.yaml.",
|
|
180
|
+
invalidNumber: "Enter a positive integer.",
|
|
181
|
+
collapse: "Collapse",
|
|
182
|
+
expand: "Expand",
|
|
183
|
+
enabledOn: "On",
|
|
184
|
+
enabledOff: "Off",
|
|
185
|
+
saveFailed: "Save failed; check the fields.",
|
|
186
|
+
confirm: "Confirm",
|
|
187
|
+
cancel: "Cancel"
|
|
188
|
+
};
|
|
189
|
+
/** own 覆盖同键合并(返回新对象,入参不可变)。 */
|
|
190
|
+
function mergeDict(base, own) {
|
|
191
|
+
return {
|
|
192
|
+
...base,
|
|
193
|
+
...own
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region ../shared/src/client/scope.ts
|
|
198
|
+
/**
|
|
199
|
+
* api 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
|
|
200
|
+
* 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
|
|
201
|
+
* 刷新:remote `settings/document-updated`(按命名空间过滤)与
|
|
202
|
+
* `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
|
|
203
|
+
* (loopback 或 tailnet 信任域名)下行为一致。
|
|
204
|
+
* effect 归属调用方 fiber(label 需含插件名以便诊断)。
|
|
205
|
+
*/
|
|
206
|
+
function createApiScope(api, ns, c, label) {
|
|
207
|
+
let state = {
|
|
208
|
+
status: "loading",
|
|
209
|
+
value: void 0,
|
|
210
|
+
revision: void 0,
|
|
211
|
+
writable: false
|
|
212
|
+
};
|
|
213
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
214
|
+
let generation = 0;
|
|
215
|
+
const publish = (next) => {
|
|
216
|
+
state = next;
|
|
217
|
+
for (const listener of [...listeners]) listener();
|
|
218
|
+
};
|
|
219
|
+
const load = async () => {
|
|
220
|
+
const gen = ++generation;
|
|
221
|
+
let response;
|
|
222
|
+
try {
|
|
223
|
+
response = await api.describe({});
|
|
224
|
+
} catch {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (gen !== generation || !response.result.ok) return;
|
|
228
|
+
const writable = response.result.value?.writable ?? false;
|
|
229
|
+
const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns);
|
|
230
|
+
if (view === void 0) {
|
|
231
|
+
publish({
|
|
232
|
+
status: "unavailable",
|
|
233
|
+
value: void 0,
|
|
234
|
+
revision: void 0,
|
|
235
|
+
writable
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
publish({
|
|
240
|
+
status: "ready",
|
|
241
|
+
value: view.value,
|
|
242
|
+
revision: view.revision,
|
|
243
|
+
writable
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
const refresh = (namespace) => {
|
|
247
|
+
if (namespace !== void 0 && namespace !== ns) return;
|
|
248
|
+
load();
|
|
249
|
+
};
|
|
250
|
+
const disposers = [c.get("remote").$on("settings/document-updated", refresh), c.on("connection/reset", () => {
|
|
251
|
+
load();
|
|
252
|
+
})];
|
|
253
|
+
c.effect(() => () => {
|
|
254
|
+
for (const dispose of disposers) dispose();
|
|
255
|
+
}, label);
|
|
256
|
+
load();
|
|
257
|
+
return {
|
|
258
|
+
getSnapshot: () => state,
|
|
259
|
+
subscribe: (listener) => {
|
|
260
|
+
listeners.add(listener);
|
|
261
|
+
return () => {
|
|
262
|
+
listeners.delete(listener);
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
load
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region ../shared/src/client/styles.ts
|
|
270
|
+
/**
|
|
271
|
+
* 配置卡片样式工厂(各插件 client 的公共收编版)。
|
|
272
|
+
* 视觉规则与迁移前逐字一致(类名前缀换为各插件自身,DOM 类名不变),
|
|
273
|
+
* 另补移动端与可达性增强(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
|
|
274
|
+
* - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
|
|
275
|
+
* - footer 按钮等宽换行、状态文字独占一行;
|
|
276
|
+
* - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
|
|
277
|
+
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
|
|
278
|
+
* @module @dsh-plus/shared/client/styles
|
|
279
|
+
*/
|
|
280
|
+
/** 生成全套卡片 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
|
|
281
|
+
function cardCss(prefix, extra = "") {
|
|
282
|
+
return `
|
|
283
|
+
.${prefix}-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
|
|
284
|
+
.${prefix}-card:hover{border-color:var(--dsw-alias-label-dimmed)}
|
|
285
|
+
.${prefix}-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
|
|
286
|
+
.${prefix}-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
|
|
287
|
+
.${prefix}-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
|
288
|
+
.${prefix}-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
289
|
+
.${prefix}-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
|
|
290
|
+
.${prefix}-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
291
|
+
.${prefix}-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;display:inline-flex}
|
|
292
|
+
.${prefix}-chevronOpen{transform:rotate(180deg)}
|
|
293
|
+
.${prefix}-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
294
|
+
.${prefix}-statusBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
295
|
+
.${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
296
|
+
.${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
|
|
297
|
+
.${prefix}-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
|
|
298
|
+
.${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
299
|
+
.${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
300
|
+
.${prefix}-head{align-items:center;gap:8px;display:flex}
|
|
301
|
+
.${prefix}-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
302
|
+
.${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
303
|
+
.${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
304
|
+
.${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
|
|
305
|
+
.${prefix}-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;transition:border-color .18s,background .18s}
|
|
306
|
+
.${prefix}-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
|
|
307
|
+
.${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
308
|
+
.${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
309
|
+
.${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
310
|
+
.${prefix}-select{appearance:none;background:var(--dsw-alias-bg-module-platform);border:none;cursor:pointer;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-secondary) 50%),linear-gradient(135deg,var(--dsw-alias-label-secondary) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;height:34px;box-sizing:border-box;border-radius:8px;padding:0 30px 0 12px;transition:background-color .18s}
|
|
311
|
+
.${prefix}-select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}
|
|
312
|
+
.${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
313
|
+
.${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
314
|
+
.${prefix}-checkRow{align-items:center;gap:10px;display:flex;padding:6px 0}
|
|
315
|
+
.${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer;flex:1;min-width:0}
|
|
316
|
+
/* 自绘圆角扁平开关(官方 UI 无可见原生 checkbox,选择类全走胶囊/菜单) */
|
|
317
|
+
.${prefix}-checkRow input{appearance:none;-webkit-appearance:none;position:relative;box-sizing:border-box;background:var(--dsw-alias-bg-module-platform);border:none;border-radius:999px;width:34px;height:20px;flex:none;cursor:pointer;transition:background .18s;margin:0}
|
|
318
|
+
.${prefix}-checkRow input::after{content:"";position:absolute;top:2px;left:2px;background:var(--dsw-alias-label-primary);border-radius:999px;width:16px;height:16px;transition:left .18s cubic-bezier(.4,0,.2,1)}
|
|
319
|
+
.${prefix}-checkRow input:checked{background:var(--dsw-alias-brand-primary)}
|
|
320
|
+
.${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
|
|
321
|
+
.${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
|
|
322
|
+
.${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
323
|
+
.${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
324
|
+
.${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
325
|
+
.${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
326
|
+
.${prefix}-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
|
|
327
|
+
.${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
|
|
328
|
+
.${prefix}-statusError{color:var(--dsw-alias-label-error)}
|
|
329
|
+
.${prefix}-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
|
|
330
|
+
.${prefix}-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
|
|
331
|
+
.${prefix}-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
|
|
332
|
+
.${prefix}-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
333
|
+
.${prefix}-btn:disabled{opacity:.4;cursor:default}
|
|
334
|
+
.${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
335
|
+
@media (max-width:767px){
|
|
336
|
+
.${prefix}-body{margin:0 10px}
|
|
337
|
+
.${prefix}-header{padding:12px 12px}
|
|
338
|
+
.${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
|
|
339
|
+
.${prefix}-checkRow{min-height:44px;padding:8px 0}
|
|
340
|
+
.${prefix}-footer{position:sticky;bottom:0;background:var(--dsw-alias-bg-layer-2);margin:0 -10px;padding:10px 10px 6px;border-top:1px solid var(--dsw-alias-border-l2);border-bottom-left-radius:12px;border-bottom-right-radius:12px}
|
|
341
|
+
.${prefix}-status{flex-basis:100%;flex:none}
|
|
342
|
+
.${prefix}-btn{min-height:44px;flex:1;min-width:96px}
|
|
343
|
+
.${prefix}-statusBadge{display:none}
|
|
344
|
+
}
|
|
345
|
+
@media (pointer:coarse){
|
|
346
|
+
.${prefix}-btn{min-height:44px}
|
|
347
|
+
.${prefix}-checkRow{min-height:44px}
|
|
348
|
+
.${prefix}-input,.${prefix}-select{height:40px}
|
|
349
|
+
}
|
|
350
|
+
${extra}
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* 幂等注入样式标签(官方 data-plugin-css 约定);返回新标签,
|
|
355
|
+
* 已存在或环境无 document 时返回 null(调用方 effect 卸载用)。
|
|
356
|
+
*/
|
|
357
|
+
function injectCardStyle(pluginId, css) {
|
|
358
|
+
if (typeof document === "undefined") return null;
|
|
359
|
+
const tagId = `${pluginId}/card.css`;
|
|
360
|
+
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) !== null) return null;
|
|
361
|
+
const tag = document.createElement("style");
|
|
362
|
+
tag.dataset.plugin = pluginId;
|
|
363
|
+
tag.dataset.pluginCss = tagId;
|
|
364
|
+
tag.textContent = css;
|
|
365
|
+
document.head.appendChild(tag);
|
|
366
|
+
return tag;
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/ns.ts
|
|
370
|
+
/**
|
|
371
|
+
* settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
|
|
372
|
+
* 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
|
|
373
|
+
* settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
|
|
374
|
+
* 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
|
|
375
|
+
* 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
|
|
376
|
+
* @module notify-email/ns
|
|
377
|
+
*/
|
|
378
|
+
const SETTINGS_NS = "dsh-plus-notify-email";
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/client/api.ts
|
|
381
|
+
const ROUTE_TEST = "/dsh-plus/notify-email/test";
|
|
382
|
+
async function parse(res) {
|
|
383
|
+
const body = await res.json();
|
|
384
|
+
if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
385
|
+
return body;
|
|
386
|
+
}
|
|
387
|
+
async function sendTest() {
|
|
388
|
+
return parse(await fetch(ROUTE_TEST, {
|
|
389
|
+
method: "POST",
|
|
390
|
+
credentials: "same-origin"
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
85
394
|
//#region src/client/card.tsx
|
|
86
395
|
/**
|
|
87
396
|
* 「邮件通知」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
397
|
+
* 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
|
|
398
|
+
* 本文件只保留业务字段与保存/测试逻辑。交互对齐官方卡片:折叠/展开、
|
|
399
|
+
* staged draft、未保存标记、保存/放弃;另加「发送测试邮件」。
|
|
400
|
+
* 配置读写走官方 settingsScope 传输:value 为 schema 解析后的脱敏视图
|
|
401
|
+
* (smtp.pass 不出现,passConfigured 由 describe 的 secrets 探测),
|
|
92
402
|
* 保存经 settings.update 深合并(空 pass 剔除 = 保持不变)。
|
|
93
403
|
* @module notify-email/client/card
|
|
94
404
|
*/
|
|
95
|
-
const IDLE_STATUS = {
|
|
96
|
-
kind: "idle",
|
|
97
|
-
text: ""
|
|
98
|
-
};
|
|
99
405
|
function draftFromValue(value) {
|
|
100
406
|
return {
|
|
101
407
|
enabled: value.enabled,
|
|
@@ -232,198 +538,179 @@ window.__ModuleLoader__.load({
|
|
|
232
538
|
}).finally(() => setTesting(false));
|
|
233
539
|
};
|
|
234
540
|
const disabled = !snapshot.writable;
|
|
235
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
type: "button",
|
|
398
|
-
className: "dne-btn dne-btnGhost",
|
|
399
|
-
disabled: !dirty || saving,
|
|
400
|
-
onClick: () => {
|
|
401
|
-
setDraft(draftFromValue(value));
|
|
402
|
-
setStatus(IDLE_STATUS);
|
|
403
|
-
},
|
|
404
|
-
children: t("discard")
|
|
405
|
-
}),
|
|
406
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
407
|
-
type: "button",
|
|
408
|
-
className: "dne-btn dne-btnPrimary",
|
|
409
|
-
disabled: !dirty || invalid || saving || disabled,
|
|
410
|
-
onClick: onSave,
|
|
411
|
-
children: t(saving ? "saving" : "save")
|
|
412
|
-
})
|
|
413
|
-
]
|
|
414
|
-
})
|
|
415
|
-
]
|
|
416
|
-
}) : null]
|
|
541
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
|
|
542
|
+
prefix: "dne",
|
|
543
|
+
title: t("title"),
|
|
544
|
+
description: t("description"),
|
|
545
|
+
open,
|
|
546
|
+
onToggle: setOpen,
|
|
547
|
+
statusBadge: {
|
|
548
|
+
text: t(draft.enabled ? "enabledOn" : "enabledOff"),
|
|
549
|
+
on: draft.enabled
|
|
550
|
+
},
|
|
551
|
+
dirty,
|
|
552
|
+
dirtyLabel: t("unsaved"),
|
|
553
|
+
readOnlyNotice: disabled ? t("readOnly") : void 0,
|
|
554
|
+
status,
|
|
555
|
+
actions: [
|
|
556
|
+
{
|
|
557
|
+
key: "test",
|
|
558
|
+
label: t(testing ? "testing" : "test"),
|
|
559
|
+
disabled: testing,
|
|
560
|
+
onClick: onTest
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
key: "discard",
|
|
564
|
+
label: t("discard"),
|
|
565
|
+
disabled: !dirty || saving,
|
|
566
|
+
onClick: () => {
|
|
567
|
+
setDraft(draftFromValue(value));
|
|
568
|
+
setStatus(IDLE_STATUS);
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
key: "save",
|
|
573
|
+
label: t(saving ? "saving" : "save"),
|
|
574
|
+
variant: "primary",
|
|
575
|
+
disabled: !dirty || invalid || saving || disabled,
|
|
576
|
+
onClick: onSave
|
|
577
|
+
}
|
|
578
|
+
],
|
|
579
|
+
children: [
|
|
580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
581
|
+
prefix: "dne",
|
|
582
|
+
id: "dne-enabled",
|
|
583
|
+
label: t("enabled"),
|
|
584
|
+
checked: draft.enabled,
|
|
585
|
+
disabled,
|
|
586
|
+
onEdit: (v) => edit("enabled", v)
|
|
587
|
+
}),
|
|
588
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
589
|
+
prefix: "dne",
|
|
590
|
+
id: "dne-host",
|
|
591
|
+
label: t("host"),
|
|
592
|
+
hint: t("hostHint"),
|
|
593
|
+
value: draft.host,
|
|
594
|
+
disabled,
|
|
595
|
+
onEdit: (v) => edit("host", v)
|
|
596
|
+
}),
|
|
597
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
598
|
+
prefix: "dne",
|
|
599
|
+
id: "dne-port",
|
|
600
|
+
label: t("port"),
|
|
601
|
+
hint: t("portHint"),
|
|
602
|
+
value: draft.port,
|
|
603
|
+
numeric: true,
|
|
604
|
+
disabled,
|
|
605
|
+
invalid: !isPositiveInt(draft.port),
|
|
606
|
+
invalidLabel: t("invalidNumber"),
|
|
607
|
+
onEdit: (v) => edit("port", v)
|
|
608
|
+
}),
|
|
609
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
610
|
+
prefix: "dne",
|
|
611
|
+
id: "dne-secure",
|
|
612
|
+
label: t("secure"),
|
|
613
|
+
checked: draft.secure,
|
|
614
|
+
disabled,
|
|
615
|
+
onEdit: (v) => edit("secure", v)
|
|
616
|
+
}),
|
|
617
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
618
|
+
prefix: "dne",
|
|
619
|
+
id: "dne-user",
|
|
620
|
+
label: t("user"),
|
|
621
|
+
hint: t("userHint"),
|
|
622
|
+
value: draft.user,
|
|
623
|
+
disabled,
|
|
624
|
+
onEdit: (v) => edit("user", v)
|
|
625
|
+
}),
|
|
626
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
627
|
+
prefix: "dne",
|
|
628
|
+
id: "dne-pass",
|
|
629
|
+
label: t("pass"),
|
|
630
|
+
hint: t("passHint"),
|
|
631
|
+
value: draft.pass,
|
|
632
|
+
password: true,
|
|
633
|
+
disabled,
|
|
634
|
+
badge: {
|
|
635
|
+
text: passConfigured ? t("passSet") : t("passUnset"),
|
|
636
|
+
set: passConfigured
|
|
637
|
+
},
|
|
638
|
+
onEdit: (v) => edit("pass", v)
|
|
639
|
+
}),
|
|
640
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
641
|
+
prefix: "dne",
|
|
642
|
+
id: "dne-from",
|
|
643
|
+
label: t("from"),
|
|
644
|
+
hint: t("fromHint"),
|
|
645
|
+
value: draft.from,
|
|
646
|
+
disabled,
|
|
647
|
+
onEdit: (v) => edit("from", v)
|
|
648
|
+
}),
|
|
649
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
650
|
+
prefix: "dne",
|
|
651
|
+
id: "dne-to",
|
|
652
|
+
label: t("to"),
|
|
653
|
+
hint: t("toHint"),
|
|
654
|
+
value: draft.toText,
|
|
655
|
+
disabled,
|
|
656
|
+
onEdit: (v) => edit("toText", v)
|
|
657
|
+
}),
|
|
658
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
659
|
+
className: "dne-groupLabel",
|
|
660
|
+
children: t("triggerGroup")
|
|
661
|
+
}),
|
|
662
|
+
TRIGGER_KEYS.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
663
|
+
prefix: "dne",
|
|
664
|
+
id: `dne-${key}`,
|
|
665
|
+
label: t(key),
|
|
666
|
+
checked: draft.triggers[key],
|
|
667
|
+
disabled,
|
|
668
|
+
onEdit: (v) => editTrigger(key, v)
|
|
669
|
+
}, key)),
|
|
670
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
671
|
+
prefix: "dne",
|
|
672
|
+
id: "dne-debounce",
|
|
673
|
+
label: t("idleDebounceMs"),
|
|
674
|
+
hint: t("idleDebounceMsHint"),
|
|
675
|
+
value: draft.idleDebounceMs,
|
|
676
|
+
numeric: true,
|
|
677
|
+
disabled,
|
|
678
|
+
invalid: !isPositiveInt(draft.idleDebounceMs),
|
|
679
|
+
invalidLabel: t("invalidNumber"),
|
|
680
|
+
onEdit: (v) => edit("idleDebounceMs", v)
|
|
681
|
+
}),
|
|
682
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
683
|
+
prefix: "dne",
|
|
684
|
+
id: "dne-maxchars",
|
|
685
|
+
label: t("maxBodyChars"),
|
|
686
|
+
hint: t("maxBodyCharsHint"),
|
|
687
|
+
value: draft.maxBodyChars,
|
|
688
|
+
numeric: true,
|
|
689
|
+
disabled,
|
|
690
|
+
invalid: !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200,
|
|
691
|
+
invalidLabel: t("invalidNumber"),
|
|
692
|
+
onEdit: (v) => edit("maxBodyChars", v)
|
|
693
|
+
}),
|
|
694
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
695
|
+
prefix: "dne",
|
|
696
|
+
id: "dne-dryrun",
|
|
697
|
+
label: t("dryRun"),
|
|
698
|
+
checked: draft.dryRun,
|
|
699
|
+
disabled,
|
|
700
|
+
onEdit: (v) => edit("dryRun", v)
|
|
701
|
+
})
|
|
702
|
+
]
|
|
417
703
|
});
|
|
418
704
|
}
|
|
419
705
|
//#endregion
|
|
420
706
|
//#region src/client/i18n.ts
|
|
421
707
|
/**
|
|
422
708
|
* 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
|
|
709
|
+
* 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
|
|
423
710
|
* @module notify-email/client/i18n
|
|
424
711
|
*/
|
|
425
712
|
const NS = "dsh-plus-notify-email";
|
|
426
|
-
const
|
|
713
|
+
const ownZh = {
|
|
427
714
|
title: "邮件通知",
|
|
428
715
|
description: "任务结束、等待决策或出错时向指定邮箱发送邮件。",
|
|
429
716
|
enabled: "启用通知",
|
|
@@ -453,25 +740,14 @@ window.__ModuleLoader__.load({
|
|
|
453
740
|
maxBodyChars: "正文截断长度",
|
|
454
741
|
maxBodyCharsHint: "邮件正文内容超过该长度时截断。",
|
|
455
742
|
dryRun: "仅记录日志(不真实发送)",
|
|
456
|
-
save: "保存",
|
|
457
|
-
saving: "保存中…",
|
|
458
|
-
discard: "放弃",
|
|
459
|
-
unsaved: "未保存",
|
|
460
743
|
test: "发送测试邮件",
|
|
461
744
|
testing: "发送中…",
|
|
462
745
|
testOk: "测试邮件已发送。",
|
|
463
746
|
testDryRun: "已按 dry-run 记录日志(未真实发送)。",
|
|
464
747
|
testFailed: "测试发送失败:",
|
|
465
|
-
saveFailed: "保存失败,请检查填写内容。",
|
|
466
|
-
loading: "加载中…",
|
|
467
|
-
unavailable: "配置服务不可用。",
|
|
468
|
-
readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
|
|
469
|
-
expand: "展开",
|
|
470
|
-
collapse: "收起",
|
|
471
|
-
invalidNumber: "请输入有效数字",
|
|
472
748
|
incomplete: "SMTP 配置不完整(服务器/发件地址/收件人必填)。"
|
|
473
749
|
};
|
|
474
|
-
const
|
|
750
|
+
const ownEn = {
|
|
475
751
|
title: "Email notifications",
|
|
476
752
|
description: "Email a mailbox when a task finishes, awaits a decision, or fails.",
|
|
477
753
|
enabled: "Enable notifications",
|
|
@@ -494,92 +770,22 @@ window.__ModuleLoader__.load({
|
|
|
494
770
|
onComplete: "Task completed",
|
|
495
771
|
onError: "Task failed",
|
|
496
772
|
onAborted: "Cancelled by user",
|
|
497
|
-
onQuestion: "
|
|
498
|
-
onPlanReview: "Plan
|
|
499
|
-
idleDebounceMs: "Idle
|
|
500
|
-
idleDebounceMsHint: "
|
|
501
|
-
maxBodyChars: "Body truncation",
|
|
502
|
-
maxBodyCharsHint: "Truncate
|
|
503
|
-
dryRun: "Log only (do not send)",
|
|
504
|
-
save: "Save",
|
|
505
|
-
saving: "Saving…",
|
|
506
|
-
discard: "Discard",
|
|
507
|
-
unsaved: "Unsaved",
|
|
773
|
+
onQuestion: "Question awaiting answer",
|
|
774
|
+
onPlanReview: "Plan awaiting review",
|
|
775
|
+
idleDebounceMs: "Idle debounce (ms)",
|
|
776
|
+
idleDebounceMsHint: "After completion, wait this long to confirm no follow-up before sending.",
|
|
777
|
+
maxBodyChars: "Body truncation length",
|
|
778
|
+
maxBodyCharsHint: "Truncate the email body beyond this length.",
|
|
779
|
+
dryRun: "Log only (do not actually send)",
|
|
508
780
|
test: "Send test email",
|
|
509
781
|
testing: "Sending…",
|
|
510
782
|
testOk: "Test email sent.",
|
|
511
|
-
testDryRun: "
|
|
512
|
-
testFailed: "Test failed: ",
|
|
513
|
-
saveFailed: "Save failed; please check the fields.",
|
|
514
|
-
loading: "Loading…",
|
|
515
|
-
unavailable: "Configuration service unavailable.",
|
|
516
|
-
readOnly: "No settings provider in this deployment; edit settings.yaml instead.",
|
|
517
|
-
expand: "Expand",
|
|
518
|
-
collapse: "Collapse",
|
|
519
|
-
invalidNumber: "Enter a valid number",
|
|
783
|
+
testDryRun: "Logged in dry-run mode (not sent).",
|
|
784
|
+
testFailed: "Test send failed: ",
|
|
520
785
|
incomplete: "SMTP config incomplete (host/from/recipients required)."
|
|
521
786
|
};
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* 配置卡片样式:沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),
|
|
526
|
-
* 视觉对齐官方卡片(--dsw-alias-* 变量),不覆盖上游任何选择器。
|
|
527
|
-
* @module notify-email/client/styles
|
|
528
|
-
*/
|
|
529
|
-
const PLUGIN_ID = "@dsh-plus/notify-email";
|
|
530
|
-
const STYLE_TAG_ID = `${PLUGIN_ID}/card.css`;
|
|
531
|
-
const cardCss = `
|
|
532
|
-
.dne-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
|
|
533
|
-
.dne-card:hover{border-color:var(--dsw-alias-label-dimmed)}
|
|
534
|
-
.dne-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
|
|
535
|
-
.dne-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
|
|
536
|
-
.dne-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
|
537
|
-
.dne-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
538
|
-
.dne-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
|
|
539
|
-
.dne-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
540
|
-
.dne-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;font-size:12px}
|
|
541
|
-
.dne-chevronOpen{transform:rotate(180deg)}
|
|
542
|
-
.dne-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
543
|
-
.dne-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
|
|
544
|
-
.dne-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
545
|
-
.dne-field+.dne-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
546
|
-
.dne-head{align-items:center;gap:8px;display:flex}
|
|
547
|
-
.dne-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
548
|
-
.dne-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
549
|
-
.dne-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
550
|
-
.dne-badgeUnset{color:var(--dsw-alias-label-tertiary)}
|
|
551
|
-
.dne-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}
|
|
552
|
-
.dne-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
553
|
-
.dne-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
554
|
-
.dne-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
555
|
-
.dne-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
556
|
-
.dne-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
557
|
-
.dne-checkRow{align-items:center;gap:8px;display:flex;padding:6px 0}
|
|
558
|
-
.dne-checkRow input{accent-color:var(--dsw-alias-brand-primary)}
|
|
559
|
-
.dne-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer}
|
|
560
|
-
.dne-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
561
|
-
.dne-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
562
|
-
.dne-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
|
|
563
|
-
.dne-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
|
|
564
|
-
.dne-statusError{color:var(--dsw-alias-label-error)}
|
|
565
|
-
.dne-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
|
|
566
|
-
.dne-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
|
|
567
|
-
.dne-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
|
|
568
|
-
.dne-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
569
|
-
.dne-btn:disabled{opacity:.4;cursor:default}
|
|
570
|
-
.dne-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
571
|
-
`;
|
|
572
|
-
/** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
|
|
573
|
-
function injectStyle() {
|
|
574
|
-
if (typeof document === "undefined") return null;
|
|
575
|
-
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) return null;
|
|
576
|
-
const tag = document.createElement("style");
|
|
577
|
-
tag.dataset.plugin = PLUGIN_ID;
|
|
578
|
-
tag.dataset.pluginCss = STYLE_TAG_ID;
|
|
579
|
-
tag.textContent = cardCss;
|
|
580
|
-
document.head.appendChild(tag);
|
|
581
|
-
return tag;
|
|
582
|
-
}
|
|
787
|
+
const zh = mergeDict(commonZh, ownZh);
|
|
788
|
+
const en = mergeDict(commonEn, ownEn);
|
|
583
789
|
//#endregion
|
|
584
790
|
//#region src/client/client.ts
|
|
585
791
|
const name = "dsh-plus-notify-email";
|
|
@@ -590,78 +796,9 @@ window.__ModuleLoader__.load({
|
|
|
590
796
|
"connection",
|
|
591
797
|
"remote"
|
|
592
798
|
];
|
|
593
|
-
/**
|
|
594
|
-
* api 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
|
|
595
|
-
* 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
|
|
596
|
-
* 刷新:remote `settings/document-updated`(按命名空间过滤)与
|
|
597
|
-
* `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
|
|
598
|
-
* (loopback 或 tailnet 信任域名)下行为一致。
|
|
599
|
-
*/
|
|
600
|
-
function createApiScope(api, ns, c) {
|
|
601
|
-
let state = {
|
|
602
|
-
status: "loading",
|
|
603
|
-
value: void 0,
|
|
604
|
-
revision: void 0,
|
|
605
|
-
writable: false
|
|
606
|
-
};
|
|
607
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
608
|
-
let generation = 0;
|
|
609
|
-
const publish = (next) => {
|
|
610
|
-
state = next;
|
|
611
|
-
for (const listener of [...listeners]) listener();
|
|
612
|
-
};
|
|
613
|
-
const load = async () => {
|
|
614
|
-
const gen = ++generation;
|
|
615
|
-
let response;
|
|
616
|
-
try {
|
|
617
|
-
response = await api.describe({});
|
|
618
|
-
} catch {
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
if (gen !== generation || !response.result.ok) return;
|
|
622
|
-
const writable = response.result.value?.writable ?? false;
|
|
623
|
-
const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns);
|
|
624
|
-
if (view === void 0) {
|
|
625
|
-
publish({
|
|
626
|
-
status: "unavailable",
|
|
627
|
-
value: void 0,
|
|
628
|
-
revision: void 0,
|
|
629
|
-
writable
|
|
630
|
-
});
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
publish({
|
|
634
|
-
status: "ready",
|
|
635
|
-
value: view.value,
|
|
636
|
-
revision: view.revision,
|
|
637
|
-
writable
|
|
638
|
-
});
|
|
639
|
-
};
|
|
640
|
-
const refresh = (namespace) => {
|
|
641
|
-
if (namespace !== void 0 && namespace !== ns) return;
|
|
642
|
-
load();
|
|
643
|
-
};
|
|
644
|
-
const disposers = [c.get("remote").$on("settings/document-updated", refresh), c.on("connection/reset", () => {
|
|
645
|
-
load();
|
|
646
|
-
})];
|
|
647
|
-
c.effect(() => () => {
|
|
648
|
-
for (const dispose of disposers) dispose();
|
|
649
|
-
}, "notify-email: settings scope");
|
|
650
|
-
load();
|
|
651
|
-
return {
|
|
652
|
-
getSnapshot: () => state,
|
|
653
|
-
subscribe: (listener) => {
|
|
654
|
-
listeners.add(listener);
|
|
655
|
-
return () => {
|
|
656
|
-
listeners.delete(listener);
|
|
657
|
-
};
|
|
658
|
-
},
|
|
659
|
-
load
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
799
|
function apply(ctx) {
|
|
663
800
|
const c = ctx;
|
|
664
|
-
const tag =
|
|
801
|
+
const tag = injectCardStyle("@dsh-plus/notify-email", cardCss("dne"));
|
|
665
802
|
c.effect(() => () => {
|
|
666
803
|
tag?.remove();
|
|
667
804
|
}, "notify-email: style");
|
|
@@ -670,7 +807,7 @@ window.__ModuleLoader__.load({
|
|
|
670
807
|
en
|
|
671
808
|
}), "notify-email: locale");
|
|
672
809
|
const api = c.get("connection").api.settings;
|
|
673
|
-
const scope = createApiScope(api, SETTINGS_NS, c);
|
|
810
|
+
const scope = createApiScope(api, SETTINGS_NS, c, "notify-email: settings scope");
|
|
674
811
|
c.slots.inject("settings.plugin.item", () => c.slots.register({
|
|
675
812
|
name: "settings.plugin.item",
|
|
676
813
|
key: SETTINGS_NS,
|