@dsh-plus/access-gate 0.1.1 → 0.1.3

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 CHANGED
@@ -4,8 +4,368 @@ 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");
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
+ }
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
+ });
91
+ }
92
+ //#endregion
93
+ //#region ../shared/src/client/fields.tsx
94
+ function TextField(props) {
95
+ const p = props.prefix ?? "dshp";
96
+ const invalid = props.invalid === true;
97
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
98
+ className: `${p}-field`,
99
+ children: [
100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
101
+ className: `${p}-head`,
102
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
103
+ className: `${p}-label`,
104
+ htmlFor: props.id,
105
+ children: props.label
106
+ }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
107
+ className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
108
+ children: props.badge.text
109
+ }) : null]
110
+ }),
111
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
112
+ id: props.id,
113
+ className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
114
+ type: props.password === true ? "password" : "text",
115
+ inputMode: props.numeric === true ? "numeric" : void 0,
116
+ autoComplete: props.password === true ? "off" : void 0,
117
+ "aria-invalid": invalid || void 0,
118
+ value: props.value,
119
+ disabled: props.disabled === true,
120
+ onChange: (event) => props.onEdit(event.target.value)
121
+ }),
122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
123
+ className: invalid ? `${p}-invalid` : `${p}-hint`,
124
+ children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
125
+ })
126
+ ]
127
+ });
128
+ }
129
+ /**
130
+ * 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
131
+ * 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
132
+ * 语义保持 checkbox(无障碍树与表单语义不变),视觉自绘。
133
+ */
134
+ function CheckRow(props) {
135
+ const p = props.prefix ?? "dshp";
136
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
137
+ className: `${p}-checkRow`,
138
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
139
+ id: props.id,
140
+ type: "checkbox",
141
+ role: "switch",
142
+ "aria-checked": props.checked,
143
+ checked: props.checked,
144
+ disabled: props.disabled === true,
145
+ onChange: (event) => props.onEdit(event.target.checked)
146
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
147
+ htmlFor: props.id,
148
+ children: props.label
149
+ })]
150
+ });
151
+ }
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
9
369
  //#region src/ns.ts
10
370
  /**
11
371
  * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
@@ -18,17 +378,14 @@ window.__ModuleLoader__.load({
18
378
  //#region src/client/card.tsx
19
379
  /**
20
380
  * 「访问控制」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
21
- * 交互对齐官方卡片:折叠/展开、staged draft、未保存标记、保存/放弃。
22
- * 配置读写走官方 settingsScope 传输(scope.ts):value 为 schema 解析后的
23
- * 脱敏视图(token 不出现,tokenConfigured 经 describe 的 secrets 探测),
381
+ * 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
382
+ * 本文件保留业务字段(token/白名单 textarea/诊断面板)与保存逻辑。
383
+ * 配置读写走官方 settingsScope 传输:value 为 schema 解析后的脱敏视图
384
+ * (token 不出现,tokenConfigured 经 describe 的 secrets 探测),
24
385
  * 保存经 settings.update 深合并(空 token 剔除 = 保持不变)。
25
386
  * 卡片顶部「当前页面诊断」读 /dsh-plus/gate/status(本页放行原因/客户端 IP)。
26
387
  * @module access-gate/client/card
27
388
  */
28
- const IDLE_STATUS = {
29
- kind: "idle",
30
- text: ""
31
- };
32
389
  function draftFromValue(value) {
33
390
  return {
34
391
  enabled: value.enabled,
@@ -140,315 +497,192 @@ window.__ModuleLoader__.load({
140
497
  }).finally(() => setSaving(false));
141
498
  };
142
499
  const disabled = !snapshot.writable;
143
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
144
- className: `dag-card${open ? " dag-cardOpen" : ""}`,
145
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
146
- type: "button",
147
- className: "dag-header",
148
- "aria-expanded": open,
149
- "aria-label": `${t(open ? "collapse" : "expand")}: ${t("title")}`,
150
- onClick: () => setOpen(!open),
151
- children: [
152
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
153
- className: "dag-headText",
154
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
155
- className: "dag-name",
156
- children: t("title")
157
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
158
- className: "dag-description",
159
- children: t("description")
160
- })]
161
- }),
162
- dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
163
- className: "dag-pending",
164
- children: t("unsaved")
165
- }) : null,
166
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
167
- className: `dag-chevron${open ? " dag-chevronOpen" : ""}`,
168
- children: "▾"
169
- })
170
- ]
171
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
172
- className: "dag-body",
173
- children: [
174
- disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
175
- className: "dag-readOnly",
176
- role: "status",
177
- children: t("readOnly")
178
- }) : null,
179
- failClosed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
180
- className: "dag-warn",
181
- children: t("warnFailClosed")
182
- }) : null,
183
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
184
- className: "dag-field",
185
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
500
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
501
+ prefix: "dag",
502
+ title: t("title"),
503
+ description: t("description"),
504
+ open,
505
+ onToggle: setOpen,
506
+ statusBadge: {
507
+ text: t(draft.enabled ? "enabledOn" : "enabledOff"),
508
+ on: draft.enabled
509
+ },
510
+ dirty,
511
+ dirtyLabel: t("unsaved"),
512
+ readOnlyNotice: disabled ? t("readOnly") : void 0,
513
+ status,
514
+ actions: [{
515
+ key: "discard",
516
+ label: t("discard"),
517
+ disabled: !dirty || saving,
518
+ onClick: () => {
519
+ setDraft(draftFromValue(value));
520
+ setStatus(IDLE_STATUS);
521
+ }
522
+ }, {
523
+ key: "save",
524
+ label: t(saving ? "saving" : "save"),
525
+ variant: "primary",
526
+ disabled: !dirty || invalid || saving || disabled,
527
+ onClick: onSave
528
+ }],
529
+ children: [
530
+ failClosed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
531
+ className: "dag-warn",
532
+ children: t("warnFailClosed")
533
+ }) : null,
534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
535
+ prefix: "dag",
536
+ id: "dag-enabled",
537
+ label: t("enabledHint"),
538
+ checked: draft.enabled,
539
+ disabled,
540
+ onEdit: (v) => edit("enabled", v)
541
+ }),
542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
543
+ prefix: "dag",
544
+ id: "dag-token",
545
+ label: t("token"),
546
+ hint: t("tokenHint"),
547
+ value: draft.token,
548
+ password: true,
549
+ disabled,
550
+ badge: {
551
+ text: t(tokenConfigured ? "tokenSet" : "tokenUnset"),
552
+ set: tokenConfigured
553
+ },
554
+ onEdit: (v) => edit("token", v)
555
+ }),
556
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
557
+ className: "dag-field",
558
+ children: [
559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
186
560
  className: "dag-head",
187
561
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
188
562
  className: "dag-label",
189
- htmlFor: "dag-enabled",
190
- children: t("enabled")
563
+ htmlFor: "dag-allowed",
564
+ children: t("allowedIps")
191
565
  })
192
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
193
- className: "dag-checkRow",
194
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
195
- id: "dag-enabled",
196
- type: "checkbox",
197
- checked: draft.enabled,
198
- disabled,
199
- onChange: (event) => edit("enabled", event.target.checked)
200
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
201
- htmlFor: "dag-enabled",
202
- children: t("enabledHint")
566
+ }),
567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
568
+ id: "dag-allowed",
569
+ className: "dag-textarea",
570
+ rows: Math.min(8, Math.max(3, draft.allowedIpsText.split("\n").length)),
571
+ value: draft.allowedIpsText,
572
+ disabled,
573
+ onChange: (event) => edit("allowedIpsText", event.target.value)
574
+ }),
575
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
576
+ className: "dag-hint",
577
+ children: t("allowedIpsHint")
578
+ })
579
+ ]
580
+ }),
581
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
582
+ prefix: "dag",
583
+ id: "dag-xff",
584
+ label: t("trustForwardedForHint"),
585
+ checked: draft.trustForwardedFor,
586
+ disabled,
587
+ onEdit: (v) => edit("trustForwardedFor", v)
588
+ }),
589
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
590
+ prefix: "dag",
591
+ id: "dag-cookie",
592
+ label: t("cookieMaxAgeHours"),
593
+ hint: t("cookieMaxAgeHoursHint"),
594
+ value: draft.cookieMaxAgeHours,
595
+ numeric: true,
596
+ disabled,
597
+ invalid: !isPositiveInt(draft.cookieMaxAgeHours),
598
+ invalidLabel: t("invalidNumber"),
599
+ onEdit: (v) => edit("cookieMaxAgeHours", v)
600
+ }),
601
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
602
+ prefix: "dag",
603
+ id: "dag-faillimit",
604
+ label: t("loginFailLimit"),
605
+ hint: t("loginFailLimitHint"),
606
+ value: draft.loginFailLimit,
607
+ numeric: true,
608
+ disabled,
609
+ invalid: !isPositiveInt(draft.loginFailLimit),
610
+ invalidLabel: t("invalidNumber"),
611
+ onEdit: (v) => edit("loginFailLimit", v)
612
+ }),
613
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
614
+ prefix: "dag",
615
+ id: "dag-cooldown",
616
+ label: t("loginCooldownMs"),
617
+ hint: t("loginCooldownMsHint"),
618
+ value: draft.loginCooldownMs,
619
+ numeric: true,
620
+ disabled,
621
+ invalid: !isPositiveInt(draft.loginCooldownMs),
622
+ invalidLabel: t("invalidNumber"),
623
+ onEdit: (v) => edit("loginCooldownMs", v)
624
+ }),
625
+ diag !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
626
+ className: `dag-diag${diag.invalidEntries.length > 0 ? " dag-diagWarn" : ""}`,
627
+ children: [
628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
629
+ className: "dag-diagTitle",
630
+ children: t("diagTitle")
631
+ }),
632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
633
+ className: "dag-diagRow",
634
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
635
+ className: "dag-diagKey",
636
+ children: t("diagVerdict")
637
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
638
+ className: "dag-diagVal",
639
+ children: verdictLabel(t, diag)
203
640
  })]
204
- })]
205
- }),
206
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
207
- className: "dag-field",
208
- children: [
209
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
210
- className: "dag-head",
211
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
212
- className: "dag-label",
213
- htmlFor: "dag-token",
214
- children: t("token")
215
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
216
- className: `dag-badge ${tokenConfigured ? "dag-badgeSet" : "dag-badgeUnset"}`,
217
- children: t(tokenConfigured ? "tokenSet" : "tokenUnset")
218
- })]
219
- }),
220
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
221
- id: "dag-token",
222
- className: "dag-input",
223
- type: "password",
224
- autoComplete: "off",
225
- value: draft.token,
226
- disabled,
227
- onChange: (event) => edit("token", event.target.value)
228
- }),
229
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
230
- className: "dag-hint",
231
- children: t("tokenHint")
232
- })
233
- ]
234
- }),
235
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
236
- className: "dag-field",
237
- children: [
238
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
239
- className: "dag-head",
240
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
241
- className: "dag-label",
242
- htmlFor: "dag-allowed",
243
- children: t("allowedIps")
244
- })
245
- }),
246
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
247
- id: "dag-allowed",
248
- className: "dag-textarea",
249
- rows: Math.min(8, Math.max(3, draft.allowedIpsText.split("\n").length)),
250
- value: draft.allowedIpsText,
251
- disabled,
252
- onChange: (event) => edit("allowedIpsText", event.target.value)
253
- }),
254
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
255
- className: "dag-hint",
256
- children: t("allowedIpsHint")
257
- })
258
- ]
259
- }),
260
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
261
- className: "dag-field",
262
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
263
- className: "dag-head",
264
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
265
- className: "dag-label",
266
- htmlFor: "dag-xff",
267
- children: t("trustForwardedFor")
268
- })
269
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
270
- className: "dag-checkRow",
271
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
272
- id: "dag-xff",
273
- type: "checkbox",
274
- checked: draft.trustForwardedFor,
275
- disabled,
276
- onChange: (event) => edit("trustForwardedFor", event.target.checked)
277
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
278
- htmlFor: "dag-xff",
279
- children: t("trustForwardedForHint")
641
+ }),
642
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
643
+ className: "dag-diagRow",
644
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
645
+ className: "dag-diagKey",
646
+ children: t("diagClientIp")
647
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
648
+ className: "dag-diagVal",
649
+ children: diag.clientIp ?? "—"
280
650
  })]
281
- })]
282
- }),
283
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
284
- className: "dag-field",
285
- children: [
286
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
287
- className: "dag-head",
288
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
289
- className: "dag-label",
290
- htmlFor: "dag-cookie",
291
- children: t("cookieMaxAgeHours")
292
- })
293
- }),
294
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
295
- id: "dag-cookie",
296
- className: `dag-input${isPositiveInt(draft.cookieMaxAgeHours) ? "" : " dag-inputInvalid"}`,
297
- type: "text",
298
- inputMode: "numeric",
299
- value: draft.cookieMaxAgeHours,
300
- disabled,
301
- "aria-invalid": !isPositiveInt(draft.cookieMaxAgeHours) || void 0,
302
- onChange: (event) => edit("cookieMaxAgeHours", event.target.value)
303
- }),
304
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
305
- className: isPositiveInt(draft.cookieMaxAgeHours) ? "dag-hint" : "dag-invalid",
306
- children: isPositiveInt(draft.cookieMaxAgeHours) ? t("cookieMaxAgeHoursHint") : t("invalidNumber")
307
- })
308
- ]
309
- }),
310
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
311
- className: "dag-field",
312
- children: [
313
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
314
- className: "dag-head",
315
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
316
- className: "dag-label",
317
- htmlFor: "dag-faillimit",
318
- children: t("loginFailLimit")
319
- })
320
- }),
321
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
322
- id: "dag-faillimit",
323
- className: `dag-input${isPositiveInt(draft.loginFailLimit) ? "" : " dag-inputInvalid"}`,
324
- type: "text",
325
- inputMode: "numeric",
326
- value: draft.loginFailLimit,
327
- disabled,
328
- "aria-invalid": !isPositiveInt(draft.loginFailLimit) || void 0,
329
- onChange: (event) => edit("loginFailLimit", event.target.value)
330
- }),
331
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
332
- className: isPositiveInt(draft.loginFailLimit) ? "dag-hint" : "dag-invalid",
333
- children: isPositiveInt(draft.loginFailLimit) ? t("loginFailLimitHint") : t("invalidNumber")
334
- })
335
- ]
336
- }),
337
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
338
- className: "dag-field",
339
- children: [
340
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
341
- className: "dag-head",
342
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
343
- className: "dag-label",
344
- htmlFor: "dag-cooldown",
345
- children: t("loginCooldownMs")
346
- })
347
- }),
348
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
349
- id: "dag-cooldown",
350
- className: `dag-input${isPositiveInt(draft.loginCooldownMs) ? "" : " dag-inputInvalid"}`,
351
- type: "text",
352
- inputMode: "numeric",
353
- value: draft.loginCooldownMs,
354
- disabled,
355
- "aria-invalid": !isPositiveInt(draft.loginCooldownMs) || void 0,
356
- onChange: (event) => edit("loginCooldownMs", event.target.value)
357
- }),
358
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
359
- className: isPositiveInt(draft.loginCooldownMs) ? "dag-hint" : "dag-invalid",
360
- children: isPositiveInt(draft.loginCooldownMs) ? t("loginCooldownMsHint") : t("invalidNumber")
361
- })
362
- ]
363
- }),
364
- diag !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
365
- className: `dag-diag${diag.invalidEntries.length > 0 ? " dag-diagWarn" : ""}`,
366
- children: [
367
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
368
- className: "dag-diagTitle",
369
- children: t("diagTitle")
370
- }),
371
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
372
- className: "dag-diagRow",
373
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
374
- className: "dag-diagKey",
375
- children: t("diagVerdict")
376
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
377
- className: "dag-diagVal",
378
- children: verdictLabel(t, diag)
379
- })]
380
- }),
381
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
382
- className: "dag-diagRow",
383
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
384
- className: "dag-diagKey",
385
- children: t("diagClientIp")
386
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
387
- className: "dag-diagVal",
388
- children: diag.clientIp ?? "—"
389
- })]
390
- }),
391
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
392
- className: "dag-diagRow",
393
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
394
- className: "dag-diagKey",
395
- children: t("diagReason")
396
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
397
- className: "dag-diagVal",
398
- children: diag.reason !== null ? t(REASON_KEYS[diag.reason] ?? diag.reason) : "—"
399
- })]
400
- }),
401
- diag.invalidEntries.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
402
- className: "dag-diagRow",
403
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
404
- className: "dag-diagKey",
405
- children: "!"
406
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
407
- className: "dag-diagVal",
408
- children: [t("diagInvalid"), diag.invalidEntries.join("、")]
409
- })]
410
- }) : null
411
- ]
412
- }) : null,
413
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
414
- className: "dag-footer",
415
- children: [
416
- status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
417
- className: `dag-status${status.kind === "error" ? " dag-statusError" : ""}`,
418
- role: "status",
419
- children: status.text
420
- }) : null,
421
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
422
- type: "button",
423
- className: "dag-btn dag-btnGhost",
424
- disabled: !dirty || saving,
425
- onClick: () => {
426
- setDraft(draftFromValue(value));
427
- setStatus(IDLE_STATUS);
428
- },
429
- children: t("discard")
430
- }),
431
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
432
- type: "button",
433
- className: "dag-btn dag-btnPrimary",
434
- disabled: !dirty || invalid || saving || disabled,
435
- onClick: onSave,
436
- children: t(saving ? "saving" : "save")
437
- })
438
- ]
439
- })
440
- ]
441
- }) : null]
651
+ }),
652
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
653
+ className: "dag-diagRow",
654
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
655
+ className: "dag-diagKey",
656
+ children: t("diagReason")
657
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
658
+ className: "dag-diagVal",
659
+ children: diag.reason !== null ? t(REASON_KEYS[diag.reason] ?? diag.reason) : "—"
660
+ })]
661
+ }),
662
+ diag.invalidEntries.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
663
+ className: "dag-diagRow",
664
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
665
+ className: "dag-diagKey",
666
+ children: "!"
667
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
668
+ className: "dag-diagVal",
669
+ children: [t("diagInvalid"), diag.invalidEntries.join("、")]
670
+ })]
671
+ }) : null
672
+ ]
673
+ }) : null
674
+ ]
442
675
  });
443
676
  }
444
677
  //#endregion
445
678
  //#region src/client/i18n.ts
446
679
  /**
447
680
  * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
681
+ * 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
448
682
  * @module access-gate/client/i18n
449
683
  */
450
684
  const NS = "dsh-plus-access-gate";
451
- const zh = {
685
+ const ownZh = {
452
686
  title: "访问控制",
453
687
  description: "Web 访问围栏:本机直连放行;远程按白名单 IP 或访问令牌放行,其余拦截到登录页。",
454
688
  enabled: "启用访问围栏",
@@ -479,20 +713,9 @@ window.__ModuleLoader__.load({
479
713
  diagLogin: "登录页",
480
714
  diagOff: "围栏未启用",
481
715
  diagInvalid: "白名单存在无法解析的条目:",
482
- warnFailClosed: "警告:已启用但令牌与白名单均为空——所有远程访问将被拒绝(含本页之外的设备)。",
483
- save: "保存",
484
- saving: "保存中…",
485
- discard: "放弃",
486
- unsaved: "未保存",
487
- saveFailed: "保存失败,请检查填写内容。",
488
- loading: "加载中…",
489
- unavailable: "配置服务不可用。",
490
- readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
491
- expand: "展开",
492
- collapse: "收起",
493
- invalidNumber: "请输入有效数字"
716
+ warnFailClosed: "警告:已启用但令牌与白名单均为空——所有远程访问将被拒绝(含本页之外的设备)。"
494
717
  };
495
- const en = {
718
+ const ownEn = {
496
719
  title: "Access Control",
497
720
  description: "Web access gate: loopback passes; remote traffic needs an allowlisted IP or the token, others hit the login page.",
498
721
  enabled: "Enable access gate",
@@ -523,71 +746,23 @@ window.__ModuleLoader__.load({
523
746
  diagLogin: "login page",
524
747
  diagOff: "gate disabled",
525
748
  diagInvalid: "Allowlist entries that failed to parse:",
526
- warnFailClosed: "Warning: enabled with no token and an empty allowlist — all remote access will be refused.",
527
- save: "Save",
528
- saving: "Saving…",
529
- discard: "Discard",
530
- unsaved: "Unsaved",
531
- saveFailed: "Save failed; please check the inputs.",
532
- loading: "Loading…",
533
- unavailable: "Settings service unavailable.",
534
- readOnly: "No settings provider in this deployment; read-only. Edit settings.yaml instead.",
535
- expand: "Expand",
536
- collapse: "Collapse",
537
- invalidNumber: "Enter a valid number"
749
+ warnFailClosed: "Warning: enabled with no token and an empty allowlist — all remote access will be refused."
538
750
  };
751
+ const zh = mergeDict(commonZh, ownZh);
752
+ const en = mergeDict(commonEn, ownEn);
539
753
  //#endregion
540
754
  //#region src/client/styles.ts
541
755
  /**
542
- * 配置卡片样式:沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),
543
- * 视觉对齐官方卡片(--dsw-alias-* 变量),不覆盖上游任何选择器。
756
+ * 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('dag'),
757
+ * 本文件只补插件特有规则(textarea / 诊断面板 / 警示条)。
758
+ * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
544
759
  * @module access-gate/client/styles
545
760
  */
546
761
  const PLUGIN_ID = "@dsh-plus/access-gate";
547
- const STYLE_TAG_ID = `${PLUGIN_ID}/card.css`;
548
- const cardCss = `
549
- .dag-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}
550
- .dag-card:hover{border-color:var(--dsw-alias-label-dimmed)}
551
- .dag-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
552
- .dag-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}
553
- .dag-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
554
- .dag-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
555
- .dag-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
556
- .dag-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
557
- .dag-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;font-size:12px}
558
- .dag-chevronOpen{transform:rotate(180deg)}
559
- .dag-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}
560
- .dag-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
561
- .dag-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
562
- .dag-field+.dag-field{border-top:1px solid var(--dsw-alias-border-l2)}
563
- .dag-head{align-items:center;gap:8px;display:flex}
564
- .dag-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
565
- .dag-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
566
- .dag-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
567
- .dag-badgeUnset{color:var(--dsw-alias-label-tertiary)}
568
- .dag-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}
569
- .dag-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
570
- .dag-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
571
- .dag-inputInvalid{border-color:var(--dsw-alias-label-error)}
762
+ const cardCssAll = cardCss("dag", `
572
763
  .dag-textarea{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.6;min-height:96px;resize:vertical;font-family:var(--dsw-alias-font-mono,ui-monospace,monospace)}
573
764
  .dag-textarea:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
574
765
  .dag-textarea:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
575
- .dag-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
576
- .dag-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
577
- .dag-checkRow{align-items:center;gap:8px;display:flex;padding:6px 0}
578
- .dag-checkRow input{accent-color:var(--dsw-alias-brand-primary)}
579
- .dag-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer}
580
- .dag-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
581
- .dag-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
582
- .dag-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}
583
- .dag-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
584
- .dag-statusError{color:var(--dsw-alias-label-error)}
585
- .dag-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
586
- .dag-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
587
- .dag-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
588
- .dag-btnPrimary{background:var(--dsh-plus-brand,#3d6dff);color:#fff}
589
- .dag-btn:disabled{opacity:.4;cursor:default}
590
- .dag-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
591
766
  .dag-diag{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;margin:12px 0 0;flex-direction:column;gap:4px;display:flex}
592
767
  .dag-diagTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;margin:0}
593
768
  .dag-diagRow{display:flex;gap:8px;font-size:12px;line-height:1.6}
@@ -595,17 +770,15 @@ window.__ModuleLoader__.load({
595
770
  .dag-diagVal{color:var(--dsw-alias-label-primary);margin:0}
596
771
  .dag-diagWarn{color:var(--dsw-alias-label-error);border-color:var(--dsw-alias-label-error)}
597
772
  .dag-warn{color:var(--dsh-plus-warn,#c9503f);border:1px solid var(--dsh-plus-warn-border,rgba(201,80,63,.4));border-radius:8px;padding:8px 12px;margin:12px 0 0;font-size:12px;line-height:1.6}
598
- `;
773
+ @media (max-width:767px){
774
+ .dag-diagRow{flex-direction:column;gap:2px}
775
+ .dag-diagKey{min-width:0}
776
+ .dag-textarea{font-size:16px}
777
+ }
778
+ `);
599
779
  /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
600
780
  function injectStyle() {
601
- if (typeof document === "undefined") return null;
602
- if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) return null;
603
- const tag = document.createElement("style");
604
- tag.dataset.plugin = PLUGIN_ID;
605
- tag.dataset.pluginCss = STYLE_TAG_ID;
606
- tag.textContent = cardCss;
607
- document.head.append(tag);
608
- return tag;
781
+ return injectCardStyle(PLUGIN_ID, cardCssAll);
609
782
  }
610
783
  //#endregion
611
784
  //#region src/client/client.ts
@@ -617,75 +790,6 @@ window.__ModuleLoader__.load({
617
790
  "connection",
618
791
  "remote"
619
792
  ];
620
- /**
621
- * api 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
622
- * 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
623
- * 刷新:remote `settings/document-updated`(按命名空间过滤)与
624
- * `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
625
- * (loopback 或 tailnet 信任域名)下行为一致。
626
- */
627
- function createApiScope(api, ns, c) {
628
- let state = {
629
- status: "loading",
630
- value: void 0,
631
- revision: void 0,
632
- writable: false
633
- };
634
- const listeners = /* @__PURE__ */ new Set();
635
- let generation = 0;
636
- const publish = (next) => {
637
- state = next;
638
- for (const listener of [...listeners]) listener();
639
- };
640
- const load = async () => {
641
- const gen = ++generation;
642
- let response;
643
- try {
644
- response = await api.describe({});
645
- } catch {
646
- return;
647
- }
648
- if (gen !== generation || !response.result.ok) return;
649
- const writable = response.result.value?.writable ?? false;
650
- const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns);
651
- if (view === void 0) {
652
- publish({
653
- status: "unavailable",
654
- value: void 0,
655
- revision: void 0,
656
- writable
657
- });
658
- return;
659
- }
660
- publish({
661
- status: "ready",
662
- value: view.value,
663
- revision: view.revision,
664
- writable
665
- });
666
- };
667
- const refresh = (namespace) => {
668
- if (namespace !== void 0 && namespace !== ns) return;
669
- load();
670
- };
671
- const disposers = [c.get("remote").$on("settings/document-updated", refresh), c.on("connection/reset", () => {
672
- load();
673
- })];
674
- c.effect(() => () => {
675
- for (const dispose of disposers) dispose();
676
- }, "access-gate: settings scope");
677
- load();
678
- return {
679
- getSnapshot: () => state,
680
- subscribe: (listener) => {
681
- listeners.add(listener);
682
- return () => {
683
- listeners.delete(listener);
684
- };
685
- },
686
- load
687
- };
688
- }
689
793
  function apply(ctx) {
690
794
  const c = ctx;
691
795
  const tag = injectStyle();
@@ -697,7 +801,7 @@ window.__ModuleLoader__.load({
697
801
  en
698
802
  }), "access-gate: locale");
699
803
  const api = c.get("connection").api.settings;
700
- const scope = createApiScope(api, SETTINGS_NS, c);
804
+ const scope = createApiScope(api, SETTINGS_NS, c, "access-gate: settings scope");
701
805
  c.slots.inject("settings.plugin.item", () => c.slots.register({
702
806
  name: "settings.plugin.item",
703
807
  key: SETTINGS_NS,