@dsh-plus/actual 0.2.7 → 0.2.9
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 +963 -227
- package/lib/index.d.ts +0 -2
- package/lib/index.js +0 -2
- package/package.json +8 -8
- package/src/client/card.tsx +7 -26
- package/src/client/client.ts +3 -26
- package/src/client/draft.ts +0 -4
- package/src/client/i18n.ts +0 -7
- package/src/client/secrets.ts +17 -121
- package/src/config.ts +0 -5
- package/src/index.ts +1 -1
package/lib/client.js
CHANGED
|
@@ -4,11 +4,36 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
16
|
+
key = keys[i];
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
18
|
+
get: ((k) => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
//#endregion
|
|
8
29
|
let react = require("react");
|
|
30
|
+
react = __toESM(react, 1);
|
|
31
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
32
|
+
let _deepseek_ai_schemastery = require("@deepseek-ai/schemastery");
|
|
33
|
+
_deepseek_ai_schemastery = __toESM(_deepseek_ai_schemastery, 1);
|
|
9
34
|
require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
35
|
//#region ../shared/src/client/fields.tsx
|
|
11
|
-
/** 卡片按钮(与
|
|
36
|
+
/** 卡片按钮(与 ConfigShell 页脚同一套类名与视觉)。 */
|
|
12
37
|
function Button(props) {
|
|
13
38
|
const p = props.prefix ?? "dshp";
|
|
14
39
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -112,11 +137,26 @@ window.__ModuleLoader__.load({
|
|
|
112
137
|
}
|
|
113
138
|
//#endregion
|
|
114
139
|
//#region ../shared/src/client/card.tsx
|
|
140
|
+
/**
|
|
141
|
+
* 配置卡片外壳与排版件:`ConfigShell`(卡片级无边框分节)、`Section`(卡片内二级
|
|
142
|
+
* 分组)、`FieldGrid`(字段排布)、`Note`(只读说明)、`ListEditor`(数组型资源
|
|
143
|
+
* 统一编辑)、`CardLoading`(取值未到达占位)。
|
|
144
|
+
*
|
|
145
|
+
* 宿主页面(`plugins.row.config` / `plugins.bundle.config` 的 page 视图)自己画
|
|
146
|
+
* 标题、图标、面包屑与页面级内边距,视图只填内容区——所以这里不再自绘带边框
|
|
147
|
+
* 卡片(会形成卡片套卡片,把移动端可用宽度逐层扣掉),也不做折叠:page 视图
|
|
148
|
+
* 本身就是表单落地页。度量对齐官方设置页(dsh-client-ui-primitives 的
|
|
149
|
+
* settings-form):小节标题 13px/600、字段区零横向内边距、页脚非 sticky。
|
|
150
|
+
*
|
|
151
|
+
* 各插件 card.tsx 只填 children 字段区与 actions;类名前缀与样式由
|
|
152
|
+
* cardCss(prefix) 配套。
|
|
153
|
+
* @module @dsh-plus/shared/client/card
|
|
154
|
+
*/
|
|
115
155
|
const IDLE_STATUS = {
|
|
116
156
|
kind: "idle",
|
|
117
157
|
text: ""
|
|
118
158
|
};
|
|
119
|
-
function
|
|
159
|
+
function ConfigShell(props) {
|
|
120
160
|
const p = props.prefix;
|
|
121
161
|
const badge = props.statusBadge;
|
|
122
162
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
@@ -172,7 +212,20 @@ window.__ModuleLoader__.load({
|
|
|
172
212
|
]
|
|
173
213
|
});
|
|
174
214
|
}
|
|
175
|
-
/**
|
|
215
|
+
/** 只读说明块(部署面字段、无编辑器字段的「为什么」)。 */
|
|
216
|
+
function Note(props) {
|
|
217
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
218
|
+
className: `${props.prefix}-note`,
|
|
219
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
220
|
+
className: `${props.prefix}-noteTitle`,
|
|
221
|
+
children: props.title
|
|
222
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
223
|
+
className: `${props.prefix}-noteBody`,
|
|
224
|
+
children: props.body
|
|
225
|
+
})]
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** 取值未到达时的占位:与 ConfigShell 同形态,避免宿主页里闪一次卡片外壳。 */
|
|
176
229
|
function CardLoading(props) {
|
|
177
230
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
178
231
|
className: `${props.prefix}-section`,
|
|
@@ -184,14 +237,744 @@ window.__ModuleLoader__.load({
|
|
|
184
237
|
});
|
|
185
238
|
}
|
|
186
239
|
//#endregion
|
|
240
|
+
//#region ../shared/src/client/code-ops.ts
|
|
241
|
+
/** 是否为普通对象(配置文档的合法节点)。 */
|
|
242
|
+
function isPlainObject(value) {
|
|
243
|
+
if (value === null || typeof value !== "object") return false;
|
|
244
|
+
const proto = Object.getPrototypeOf(value);
|
|
245
|
+
return proto === Object.prototype || proto === null;
|
|
246
|
+
}
|
|
247
|
+
/** 从 V8 语法错误消息中取行列;新版带 `(line L column C)`,旧版只有偏移量。 */
|
|
248
|
+
function positionOf(message, text) {
|
|
249
|
+
const located = /\(line (\d+) column (\d+)\)/.exec(message);
|
|
250
|
+
if (located !== null) return {
|
|
251
|
+
line: Number(located[1]),
|
|
252
|
+
column: Number(located[2])
|
|
253
|
+
};
|
|
254
|
+
const offset = /at position (\d+)/.exec(message);
|
|
255
|
+
if (offset === null) return {
|
|
256
|
+
line: null,
|
|
257
|
+
column: null
|
|
258
|
+
};
|
|
259
|
+
const index = Math.min(Number(offset[1]), text.length);
|
|
260
|
+
const head = text.slice(0, index);
|
|
261
|
+
return {
|
|
262
|
+
line: head.split("\n").length,
|
|
263
|
+
column: index - head.lastIndexOf("\n")
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 解析配置文档:必须是 JSON 对象(配置根节点为行级 Config 对象)。
|
|
268
|
+
* @param text - 编辑器全文。
|
|
269
|
+
* @returns 成功给出对象;失败给出原因与可定位的行列。
|
|
270
|
+
*/
|
|
271
|
+
function parseConfigJson(text) {
|
|
272
|
+
if (text.trim() === "") return {
|
|
273
|
+
ok: false,
|
|
274
|
+
reason: "empty",
|
|
275
|
+
message: "",
|
|
276
|
+
line: null,
|
|
277
|
+
column: null
|
|
278
|
+
};
|
|
279
|
+
let parsed;
|
|
280
|
+
try {
|
|
281
|
+
parsed = JSON.parse(text);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
reason: "syntax",
|
|
287
|
+
message,
|
|
288
|
+
...positionOf(message, text)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (!isPlainObject(parsed)) return {
|
|
292
|
+
ok: false,
|
|
293
|
+
reason: "root",
|
|
294
|
+
message: "",
|
|
295
|
+
line: 1,
|
|
296
|
+
column: 1
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
ok: true,
|
|
300
|
+
value: parsed
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/** 两空格缩进格式化(解析失败时原样返回输入与失败信息)。 */
|
|
304
|
+
function formatConfigJson(text) {
|
|
305
|
+
const parsed = parseConfigJson(text);
|
|
306
|
+
if (!parsed.ok) return {
|
|
307
|
+
ok: false,
|
|
308
|
+
result: parsed
|
|
309
|
+
};
|
|
310
|
+
return {
|
|
311
|
+
ok: true,
|
|
312
|
+
text: `${JSON.stringify(parsed.value, null, 2)}\n`
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** 差分:对象删键 → unset;数组删元素 → 下标降序 unset(保持其余下标有效)。 */
|
|
316
|
+
function diffToOps(before, after) {
|
|
317
|
+
const ops = [];
|
|
318
|
+
walk(before, after, [], ops);
|
|
319
|
+
return ops;
|
|
320
|
+
}
|
|
321
|
+
function walk(before, after, path, ops) {
|
|
322
|
+
if (Array.isArray(after)) {
|
|
323
|
+
walkArray(before, after, path, ops);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (isPlainObject(after)) {
|
|
327
|
+
walkObject(before, after, path, ops);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!Object.is(before, after)) ops.push({
|
|
331
|
+
op: "set",
|
|
332
|
+
path: [...path],
|
|
333
|
+
value: after
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function walkObject(before, after, path, ops) {
|
|
337
|
+
if (!isPlainObject(before)) {
|
|
338
|
+
ops.push({
|
|
339
|
+
op: "set",
|
|
340
|
+
path: [...path],
|
|
341
|
+
value: after
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
for (const [key, value] of Object.entries(after)) {
|
|
346
|
+
if (!Object.hasOwn(before, key)) {
|
|
347
|
+
ops.push({
|
|
348
|
+
op: "set",
|
|
349
|
+
path: [...path, key],
|
|
350
|
+
value
|
|
351
|
+
});
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
walk(before[key], value, [...path, key], ops);
|
|
355
|
+
}
|
|
356
|
+
for (const key of Object.keys(before)) if (!Object.hasOwn(after, key)) ops.push({
|
|
357
|
+
op: "unset",
|
|
358
|
+
path: [...path, key]
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function walkArray(before, after, path, ops) {
|
|
362
|
+
if (!Array.isArray(before)) {
|
|
363
|
+
ops.push({
|
|
364
|
+
op: "set",
|
|
365
|
+
path: [...path],
|
|
366
|
+
value: after
|
|
367
|
+
});
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const shared = Math.min(before.length, after.length);
|
|
371
|
+
for (let index = 0; index < shared; index += 1) walk(before[index], after[index], [...path, String(index)], ops);
|
|
372
|
+
for (let index = before.length - 1; index >= after.length; index -= 1) ops.push({
|
|
373
|
+
op: "unset",
|
|
374
|
+
path: [...path, String(index)]
|
|
375
|
+
});
|
|
376
|
+
for (let index = before.length; index < after.length; index += 1) ops.push({
|
|
377
|
+
op: "set",
|
|
378
|
+
path: [...path, String(index)],
|
|
379
|
+
value: after[index]
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/** schema JSON 里的密钥字段路径(`meta.role === 'secret'`):源码编辑器不呈现它们。 */
|
|
383
|
+
function secretPathsOf(schemaJson) {
|
|
384
|
+
const refs = refsOf(schemaJson);
|
|
385
|
+
if (refs === null) return [];
|
|
386
|
+
const root = schemaJson.uid;
|
|
387
|
+
if (typeof root !== "number") return [];
|
|
388
|
+
const out = [];
|
|
389
|
+
collectSecretPaths(refs, root, [], out, /* @__PURE__ */ new Set());
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 文档里 schema 未声明的键路径。
|
|
394
|
+
*
|
|
395
|
+
* schemastery 对未声明键**透传忽略**(不报错、不剥离),所以这类键会被原样写进
|
|
396
|
+
* 用户层并永久留存——典型来源是历史配置键与手写拼写错误。源码编辑器把它作为
|
|
397
|
+
* 非阻断警告列出来(迁移期新旧键并存时不至于挡住应用)。
|
|
398
|
+
*/
|
|
399
|
+
function unknownKeysOf(schemaJson, value) {
|
|
400
|
+
const refs = refsOf(schemaJson);
|
|
401
|
+
if (refs === null || !isPlainObject(value)) return [];
|
|
402
|
+
const root = schemaJson.uid;
|
|
403
|
+
if (typeof root !== "number") return [];
|
|
404
|
+
return [...unknownKeysAt(refs, root, value)].sort();
|
|
405
|
+
}
|
|
406
|
+
/** 返回「该节点下所有未被 schema 声明的键路径」的集合。 */
|
|
407
|
+
function unknownKeysAt(refs, index, value) {
|
|
408
|
+
const node = refs[String(index)];
|
|
409
|
+
if (node === void 0) return /* @__PURE__ */ new Set();
|
|
410
|
+
if (node.list !== void 0) return intersectBranches(refs, node.list, value);
|
|
411
|
+
if (node.inner !== void 0) {
|
|
412
|
+
if (Array.isArray(value)) {
|
|
413
|
+
const out = /* @__PURE__ */ new Set();
|
|
414
|
+
for (const [at, item] of value.entries()) for (const path of unknownKeysAt(refs, node.inner, item)) out.add(`${at}.${path}`);
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
return unknownKeysAt(refs, node.inner, value);
|
|
418
|
+
}
|
|
419
|
+
if (node.dict === void 0 || !isPlainObject(value)) return /* @__PURE__ */ new Set();
|
|
420
|
+
const out = /* @__PURE__ */ new Set();
|
|
421
|
+
for (const [key, child] of Object.entries(value)) {
|
|
422
|
+
const next = node.dict[key];
|
|
423
|
+
if (next === void 0) {
|
|
424
|
+
out.add(key);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
for (const path of unknownKeysAt(refs, next, child)) out.add(`${key}.${path}`);
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
/** 联合类型:只在「对象型分支」之间取交集(标量分支不否决键判定)。 */
|
|
432
|
+
function intersectBranches(refs, list, value) {
|
|
433
|
+
const objectBranches = list.filter((branch) => refs[String(branch)]?.dict !== void 0);
|
|
434
|
+
if (objectBranches.length === 0) return /* @__PURE__ */ new Set();
|
|
435
|
+
let result = null;
|
|
436
|
+
for (const branch of objectBranches) {
|
|
437
|
+
const keys = unknownKeysAt(refs, branch, value);
|
|
438
|
+
if (result === null) {
|
|
439
|
+
result = keys;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
for (const key of [...result]) if (!keys.has(key)) result.delete(key);
|
|
443
|
+
}
|
|
444
|
+
return result ?? /* @__PURE__ */ new Set();
|
|
445
|
+
}
|
|
446
|
+
function refsOf(schemaJson) {
|
|
447
|
+
if (!isPlainObject(schemaJson)) return null;
|
|
448
|
+
const refs = schemaJson.refs;
|
|
449
|
+
return isPlainObject(refs) ? refs : null;
|
|
450
|
+
}
|
|
451
|
+
function collectSecretPaths(refs, index, path, out, seen) {
|
|
452
|
+
const node = refs[String(index)];
|
|
453
|
+
if (node === void 0) return;
|
|
454
|
+
const marker = `${index}:${path.join(".")}`;
|
|
455
|
+
if (seen.has(marker)) return;
|
|
456
|
+
seen.add(marker);
|
|
457
|
+
if (node.meta?.role === "secret") {
|
|
458
|
+
out.push([...path]);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
for (const [key, child] of Object.entries(node.dict ?? {})) collectSecretPaths(refs, child, [...path, key], out, seen);
|
|
462
|
+
if (typeof node.inner === "number") collectSecretPaths(refs, node.inner, path, out, seen);
|
|
463
|
+
for (const child of node.list ?? []) collectSecretPaths(refs, child, path, out, seen);
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region ../shared/src/client/code-validate.ts
|
|
467
|
+
/**
|
|
468
|
+
* 配置源码编辑的模式校验:用宿主 `describe` 回传的 schema JSON 在浏览器本地
|
|
469
|
+
* 重建同一套 schemastery 校验(库 23KB 纯 JS,随 client bundle 打苞——浏览器端
|
|
470
|
+
* 没有宿主实例,不存在跨实例符号身份问题,见 docs/reference/官方机制参照.md
|
|
471
|
+
* 「客户端平台」)。
|
|
472
|
+
*
|
|
473
|
+
* 校验与宿主写入路径同源:`new Schema(json)` 后按同一份 volatile 投影调用,
|
|
474
|
+
* 因此本地拦下的错误与宿主 `settings/rejected` 的判据一致。
|
|
475
|
+
* @module @dsh-plus/shared/client/code-validate
|
|
476
|
+
*/
|
|
477
|
+
/** 重建结果按 schema JSON 对象身份缓存(describe 每次应答一个新对象,无泄漏风险)。 */
|
|
478
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
479
|
+
function rebuild(schemaJson) {
|
|
480
|
+
if (!isPlainObject(schemaJson)) return null;
|
|
481
|
+
const cached = cache.get(schemaJson);
|
|
482
|
+
if (cached !== void 0) return cached;
|
|
483
|
+
let built = null;
|
|
484
|
+
try {
|
|
485
|
+
built = new _deepseek_ai_schemastery.default(schemaJson);
|
|
486
|
+
} catch {
|
|
487
|
+
built = null;
|
|
488
|
+
}
|
|
489
|
+
cache.set(schemaJson, built);
|
|
490
|
+
return built;
|
|
491
|
+
}
|
|
492
|
+
/** 从 schemastery 错误消息里取路径前缀:`$.a.b expected number but got x` → `a.b`。 */
|
|
493
|
+
function pathOf(message) {
|
|
494
|
+
const located = /^\$\.([^\s]+)/.exec(message);
|
|
495
|
+
return located === null ? "" : located[1] ?? "";
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 按宿主 schema 校验一份配置文档。
|
|
499
|
+
* @param schemaJson - `describe` 应答里的 `schema`(schemastery `toJSON()` 形态)。
|
|
500
|
+
* @param value - 编辑器解析出的对象。
|
|
501
|
+
* @returns 通过(含「未校验」标记)或问题清单。
|
|
502
|
+
*/
|
|
503
|
+
function validateAgainstSchema(schemaJson, value) {
|
|
504
|
+
const validate = rebuild(schemaJson);
|
|
505
|
+
if (validate === null) return {
|
|
506
|
+
ok: true,
|
|
507
|
+
validated: false
|
|
508
|
+
};
|
|
509
|
+
try {
|
|
510
|
+
validate(value);
|
|
511
|
+
return {
|
|
512
|
+
ok: true,
|
|
513
|
+
validated: true
|
|
514
|
+
};
|
|
515
|
+
} catch (error) {
|
|
516
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
validated: true,
|
|
520
|
+
issues: [{
|
|
521
|
+
path: pathOf(message),
|
|
522
|
+
message
|
|
523
|
+
}]
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region ../shared/src/client/styles.ts
|
|
529
|
+
/**
|
|
530
|
+
* 配置分节样式工厂(各插件 client 的公共收编版)。
|
|
531
|
+
*
|
|
532
|
+
* 目标宿主 dsh 0.2.1-alpha.2:插件页把配置视图放进已有页面级内边距的容器里
|
|
533
|
+
* (`div.detailSections[data-plugin-config]` 或 `section.detailSection[data-plugin-config]`),
|
|
534
|
+
* 外层无边框——故这里输出的是**无边框分节**(小节标题 13px/600、字段 12px 0、
|
|
535
|
+
* 横向零内边距、页脚非 sticky),度量对齐官方设置页
|
|
536
|
+
* (`@deepseek-ai/dsh-client-ui-primitives` 的 settings-form)。
|
|
537
|
+
*
|
|
538
|
+
* 移动端与可达性(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
|
|
539
|
+
* - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
|
|
540
|
+
* - 页脚按钮等宽换行、状态文字独占一行。
|
|
541
|
+
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
|
|
542
|
+
* @module @dsh-plus/shared/client/styles
|
|
543
|
+
*/
|
|
544
|
+
/** 生成全套分节 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
|
|
545
|
+
function cardCss(prefix, extra = "") {
|
|
546
|
+
return `
|
|
547
|
+
.${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
|
|
548
|
+
.${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
549
|
+
.${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
|
|
550
|
+
.${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
551
|
+
.${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
|
|
552
|
+
.${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
|
|
553
|
+
.${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
|
|
554
|
+
.${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}
|
|
555
|
+
.${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
556
|
+
.${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
|
|
557
|
+
.${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
558
|
+
.${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
559
|
+
.${prefix}-head{align-items:center;gap:8px;display:flex}
|
|
560
|
+
.${prefix}-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
561
|
+
.${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
562
|
+
.${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
563
|
+
.${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
|
|
564
|
+
.${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}
|
|
565
|
+
.${prefix}-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
|
|
566
|
+
.${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
567
|
+
.${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
568
|
+
.${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
569
|
+
.${prefix}-textarea{box-sizing:border-box;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;height:auto;line-height:1.6;min-height:120px;padding:8px 12px;resize:vertical;white-space:pre;width:100%}
|
|
570
|
+
.${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}
|
|
571
|
+
.${prefix}-select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}
|
|
572
|
+
.${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
573
|
+
.${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
574
|
+
.${prefix}-checkRow{align-items:center;gap:10px;display:flex;padding:6px 0}
|
|
575
|
+
.${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer;flex:1;min-width:0}
|
|
576
|
+
/* 自绘圆角扁平开关(官方 UI 无可见原生 checkbox,选择类全走胶囊/菜单) */
|
|
577
|
+
.${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}
|
|
578
|
+
.${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)}
|
|
579
|
+
.${prefix}-checkRow input:checked{background:var(--dsw-alias-brand-primary)}
|
|
580
|
+
.${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
|
|
581
|
+
.${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
|
|
582
|
+
.${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
583
|
+
.${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
584
|
+
/* 分节(Section):卡片内的二级分组,与一级分节同度量、无边框 */
|
|
585
|
+
.${prefix}-sub{flex-direction:column;padding:12px 0 0;display:flex}
|
|
586
|
+
.${prefix}-subHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
587
|
+
.${prefix}-subTitle{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;font-weight:600;line-height:1.5}
|
|
588
|
+
.${prefix}-subDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
|
|
589
|
+
.${prefix}-subBody{flex-direction:column;min-width:0;display:flex}
|
|
590
|
+
.${prefix}-subToggle{appearance:none;background:0 0;border:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font:inherit;font-size:12px;padding:2px 6px;border-radius:6px}
|
|
591
|
+
.${prefix}-subToggle:hover{color:var(--dsw-alias-label-primary)}
|
|
592
|
+
/* 字段排布(FieldGrid):窄屏单列,宽屏可两列 */
|
|
593
|
+
.${prefix}-grid{display:grid;grid-template-columns:1fr;min-width:0}
|
|
594
|
+
@media (min-width:768px){
|
|
595
|
+
.${prefix}-grid2{grid-template-columns:1fr 1fr;column-gap:16px}
|
|
596
|
+
}
|
|
597
|
+
/* 只读说明块(Note) */
|
|
598
|
+
.${prefix}-note{background:var(--dsw-alias-bg-module-platform);border-radius:8px;margin:12px 0 0;padding:10px 12px}
|
|
599
|
+
.${prefix}-noteTitle{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;font-weight:600;line-height:1.5}
|
|
600
|
+
.${prefix}-noteBody{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5;white-space:pre-wrap}
|
|
601
|
+
/* 列表编辑器(ListEditor):条目头 + 折叠 + 行内操作 */
|
|
602
|
+
.${prefix}-list{flex-direction:column;gap:8px;padding:8px 0 0;display:flex}
|
|
603
|
+
.${prefix}-listItem{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 12px}
|
|
604
|
+
.${prefix}-listItemHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
605
|
+
.${prefix}-listItemTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
606
|
+
.${prefix}-listItemBody{flex-direction:column;min-width:0;display:flex;padding-top:4px}
|
|
607
|
+
.${prefix}-listActions{align-items:center;gap:4px;display:flex;flex-wrap:wrap}
|
|
608
|
+
.${prefix}-iconBtn{appearance:none;background:0 0;border:1px solid #0000;border-radius:6px;color:var(--dsw-alias-label-tertiary);cursor:pointer;font:inherit;font-size:12px;line-height:1.5;padding:2px 8px}
|
|
609
|
+
.${prefix}-iconBtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}
|
|
610
|
+
.${prefix}-iconBtn:disabled{opacity:.4;cursor:default}
|
|
611
|
+
.${prefix}-listAdd{align-self:flex-start}
|
|
612
|
+
.${prefix}-listAddRow{align-items:center;align-self:flex-start;gap:8px;display:flex;flex-wrap:wrap}
|
|
613
|
+
.${prefix}-listEmpty{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5;padding:4px 0}
|
|
614
|
+
.${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
615
|
+
.${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
616
|
+
.${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
|
|
617
|
+
.${prefix}-statusError{color:var(--dsw-alias-label-error)}
|
|
618
|
+
.${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}
|
|
619
|
+
.${prefix}-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
|
|
620
|
+
.${prefix}-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
|
|
621
|
+
.${prefix}-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
622
|
+
.${prefix}-btn:disabled{opacity:.4;cursor:default}
|
|
623
|
+
.${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
624
|
+
@media (max-width:767px){
|
|
625
|
+
.${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
|
|
626
|
+
.${prefix}-checkRow{min-height:44px;padding:8px 0}
|
|
627
|
+
.${prefix}-status{flex-basis:100%;flex:none}
|
|
628
|
+
.${prefix}-btn{min-height:44px;flex:1;min-width:96px}
|
|
629
|
+
}
|
|
630
|
+
@media (pointer:coarse){
|
|
631
|
+
.${prefix}-btn{min-height:44px}
|
|
632
|
+
.${prefix}-checkRow{min-height:44px}
|
|
633
|
+
.${prefix}-input,.${prefix}-select{height:40px}
|
|
634
|
+
}
|
|
635
|
+
${extra}
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
/** 源码编辑节的固定类名前缀。 */
|
|
639
|
+
const CODE_SECTION_PREFIX = "dshc";
|
|
640
|
+
/**
|
|
641
|
+
* 源码编辑节(ConfigCodeSection)专用样式:前缀固定 `dshc`,由 shared 自行注入
|
|
642
|
+
* (插件卡片不必知道它的存在)。基础规则复用 cardCss,只追加 details/摘要与
|
|
643
|
+
* 代码文本域的度量。
|
|
644
|
+
*/
|
|
645
|
+
function codeSectionCss() {
|
|
646
|
+
return cardCss(CODE_SECTION_PREFIX, `
|
|
647
|
+
.${CODE_SECTION_PREFIX}-details{padding:4px 0 0}
|
|
648
|
+
.${CODE_SECTION_PREFIX}-summary{color:var(--dsw-alias-label-primary);cursor:pointer;font-size:13px;font-weight:600;line-height:1.5;list-style:none;padding:8px 0}
|
|
649
|
+
.${CODE_SECTION_PREFIX}-summary::-webkit-details-marker{display:none}
|
|
650
|
+
.${CODE_SECTION_PREFIX}-summary::before{content:"▸";color:var(--dsw-alias-label-tertiary);display:inline-block;margin-right:6px;transition:transform .18s}
|
|
651
|
+
.${CODE_SECTION_PREFIX}-details[open] .${CODE_SECTION_PREFIX}-summary::before{transform:rotate(90deg)}
|
|
652
|
+
.${CODE_SECTION_PREFIX}-desc{color:var(--dsw-alias-label-tertiary);margin:0 0 8px;font-size:12px;line-height:1.5}
|
|
653
|
+
.${CODE_SECTION_PREFIX}-input{box-sizing:border-box;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;height:auto;line-height:1.6;min-height:240px;padding:10px 12px;resize:vertical;tab-size:2;white-space:pre;width:100%}
|
|
654
|
+
.${CODE_SECTION_PREFIX}-foot{align-items:center;gap:8px;display:flex;flex-wrap:wrap;padding-top:12px}
|
|
655
|
+
.${CODE_SECTION_PREFIX}-foot .${CODE_SECTION_PREFIX}-btn:last-child{margin-left:auto}
|
|
656
|
+
@media (max-width:767px){
|
|
657
|
+
.${CODE_SECTION_PREFIX}-input{font-size:16px}
|
|
658
|
+
}
|
|
659
|
+
`);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* 幂等注入样式标签(官方 data-plugin-css 约定);返回新标签,
|
|
663
|
+
* 已存在或环境无 document 时返回 null(调用方 effect 卸载用)。
|
|
664
|
+
*/
|
|
665
|
+
function injectCardStyle(pluginId, css) {
|
|
666
|
+
if (typeof document === "undefined") return null;
|
|
667
|
+
const tagId = `${pluginId}/card.css`;
|
|
668
|
+
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) !== null) return null;
|
|
669
|
+
const tag = document.createElement("style");
|
|
670
|
+
tag.dataset.plugin = pluginId;
|
|
671
|
+
tag.dataset.pluginCss = tagId;
|
|
672
|
+
tag.textContent = css;
|
|
673
|
+
document.head.appendChild(tag);
|
|
674
|
+
return tag;
|
|
675
|
+
}
|
|
676
|
+
//#endregion
|
|
677
|
+
//#region ../shared/src/client/code-section.ts
|
|
678
|
+
/**
|
|
679
|
+
* 配置源码编辑节(自动追加到每张插件配置卡片页)。
|
|
680
|
+
*
|
|
681
|
+
* 形态:无边框分节,与 ConfigShell/Section 的小节度量一致;文档=宿主 `describe` 回传的
|
|
682
|
+
* volatile 投影(JSON,两空格缩进),校验=本地 JSON 语法 + schemastery 模式,
|
|
683
|
+
* 写回=官方 `settings.mutate` 路径 ops(密钥字段不进文档,也不会被写坏)。
|
|
684
|
+
*
|
|
685
|
+
* 本文件刻意不写 JSX:`@dsh-plus/shared` 的 client 纯逻辑测试用 `node --test`
|
|
686
|
+
* 直跑 .ts(Node 原生类型擦除不支持 JSX),而 `config-slots.ts` 会被测试导入,
|
|
687
|
+
* 因此这条导入链上的模块一律用 `createElement`。React 只经默认导入取用
|
|
688
|
+
* (react 是 CJS,Node ESM 下具名导入不保证可解析)。
|
|
689
|
+
* @module @dsh-plus/shared/client/code-section
|
|
690
|
+
*/
|
|
691
|
+
const { createElement: h$1, useEffect: useEffect$2, useMemo: useMemo$1, useState: useState$3, useSyncExternalStore: useSyncExternalStore$1 } = react.default;
|
|
692
|
+
const IDLE = {
|
|
693
|
+
kind: "idle",
|
|
694
|
+
text: ""
|
|
695
|
+
};
|
|
696
|
+
/** 文档播种文本:对象 → 两空格缩进 JSON。 */
|
|
697
|
+
function seedText(value) {
|
|
698
|
+
return value === void 0 ? "{}\n" : `${JSON.stringify(value, null, 2)}\n`;
|
|
699
|
+
}
|
|
700
|
+
/** 解析/校验失败的原因文案(逐条给出行列或字段路径)。 */
|
|
701
|
+
function problemLines(text, snapshot, t) {
|
|
702
|
+
const parsed = parseConfigJson(text);
|
|
703
|
+
if (!parsed.ok) {
|
|
704
|
+
if (parsed.reason === "empty") return {
|
|
705
|
+
problems: [t("codeInvalidEmpty")],
|
|
706
|
+
validated: false
|
|
707
|
+
};
|
|
708
|
+
if (parsed.reason === "root") return {
|
|
709
|
+
problems: [t("codeInvalidRoot")],
|
|
710
|
+
validated: false
|
|
711
|
+
};
|
|
712
|
+
const line = t("codeLine").replace("{line}", String(parsed.line ?? 0)).replace("{column}", String(parsed.column ?? 0));
|
|
713
|
+
return {
|
|
714
|
+
problems: [`${t("codeInvalidJson")}(${line})${parsed.message}`],
|
|
715
|
+
validated: false
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
const result = validateAgainstSchema(snapshot.schema, parsed.value);
|
|
719
|
+
if (result.ok) return {
|
|
720
|
+
problems: [],
|
|
721
|
+
validated: result.validated
|
|
722
|
+
};
|
|
723
|
+
return {
|
|
724
|
+
problems: result.issues.map((issue) => `${t("codeInvalidSchema")}:${issue.path === "" ? issue.message : `${issue.path} — ${issue.message}`}`),
|
|
725
|
+
validated: true
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/** 与当前快照的差分(无改动时为空数组)。 */
|
|
729
|
+
function opsOf(text, snapshot) {
|
|
730
|
+
const parsed = parseConfigJson(text);
|
|
731
|
+
return parsed.ok ? diffToOps(snapshot.value, parsed.value) : [];
|
|
732
|
+
}
|
|
733
|
+
/** 应用:官方路径 ops 写回,成功后重载快照(revision 变化会重新播种文档)。 */
|
|
734
|
+
function applyOps(options) {
|
|
735
|
+
const { api, scope, ops, revision, t, setStatus, setApplying } = options;
|
|
736
|
+
setApplying(true);
|
|
737
|
+
api.mutate(ops, revision).then(() => {
|
|
738
|
+
setStatus({
|
|
739
|
+
kind: "ok",
|
|
740
|
+
text: t("codeApplied")
|
|
741
|
+
});
|
|
742
|
+
return scope.load();
|
|
743
|
+
}).catch((error) => {
|
|
744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
745
|
+
setStatus({
|
|
746
|
+
kind: "error",
|
|
747
|
+
text: `${t("codeApplyFailed")}${message}`
|
|
748
|
+
});
|
|
749
|
+
}).finally(() => setApplying(false));
|
|
750
|
+
}
|
|
751
|
+
/** 草稿动作:应用(官方路径 ops 写回)、格式化、编辑与 Tab 缩进。 */
|
|
752
|
+
function draftHandlers(options) {
|
|
753
|
+
const { api, scope, t, snapshot, text, ops, setText, setStatus, setApplying } = options;
|
|
754
|
+
return {
|
|
755
|
+
onApply: () => {
|
|
756
|
+
applyOps({
|
|
757
|
+
api,
|
|
758
|
+
scope,
|
|
759
|
+
ops,
|
|
760
|
+
revision: snapshot.revision,
|
|
761
|
+
t,
|
|
762
|
+
setStatus,
|
|
763
|
+
setApplying
|
|
764
|
+
});
|
|
765
|
+
},
|
|
766
|
+
onFormat: () => {
|
|
767
|
+
const formatted = formatConfigJson(text);
|
|
768
|
+
if (formatted.ok) setText(formatted.text);
|
|
769
|
+
},
|
|
770
|
+
onChange: (event) => {
|
|
771
|
+
setText(event.target.value);
|
|
772
|
+
setStatus(IDLE);
|
|
773
|
+
},
|
|
774
|
+
onKeyDown: (event) => {
|
|
775
|
+
if (event.key !== "Tab") return;
|
|
776
|
+
event.preventDefault();
|
|
777
|
+
const target = event.currentTarget;
|
|
778
|
+
const { selectionStart, selectionEnd, value } = target;
|
|
779
|
+
setText(`${value.slice(0, selectionStart)} ${value.slice(selectionEnd)}`);
|
|
780
|
+
queueMicrotask(() => {
|
|
781
|
+
target.selectionStart = selectionStart + 2;
|
|
782
|
+
target.selectionEnd = selectionStart + 2;
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
/** 校验派生量:逐次编辑重算(problems 供提示与禁用判定,validated 区分「模式外」)。 */
|
|
788
|
+
function useCodeChecks(text, snapshot, t) {
|
|
789
|
+
const { problems, validated } = useMemo$1(() => problemLines(text, snapshot, t), [
|
|
790
|
+
text,
|
|
791
|
+
snapshot,
|
|
792
|
+
t
|
|
793
|
+
]);
|
|
794
|
+
return {
|
|
795
|
+
problems,
|
|
796
|
+
validated,
|
|
797
|
+
ops: useMemo$1(() => opsOf(text, snapshot), [text, snapshot]),
|
|
798
|
+
secrets: useMemo$1(() => secretPathsOf(snapshot.schema), [snapshot.schema]),
|
|
799
|
+
unknown: useMemo$1(() => {
|
|
800
|
+
const parsed = parseConfigJson(text);
|
|
801
|
+
return parsed.ok ? unknownKeysOf(snapshot.schema, parsed.value) : [];
|
|
802
|
+
}, [text, snapshot.schema])
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
/** 分节状态:快照订阅、文档文本、校验结果与草稿动作(主组件只做组装)。 */
|
|
806
|
+
function useCodeSection(props) {
|
|
807
|
+
const { ns, t, scope, api } = props;
|
|
808
|
+
const snapshot = useSyncExternalStore$1((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
809
|
+
const [text, setText] = useState$3(() => seedText(snapshot.value));
|
|
810
|
+
const [status, setStatus] = useState$3(IDLE);
|
|
811
|
+
const [applying, setApplying] = useState$3(false);
|
|
812
|
+
const [open, setOpen] = useState$3(false);
|
|
813
|
+
const revision = snapshot.revision;
|
|
814
|
+
const snapshotStatus = snapshot.status;
|
|
815
|
+
useEffect$2(() => {
|
|
816
|
+
setText(seedText(snapshot.value));
|
|
817
|
+
setStatus(IDLE);
|
|
818
|
+
}, [revision, snapshotStatus]);
|
|
819
|
+
useEffect$2(() => {
|
|
820
|
+
injectCardStyle("@dsh-plus/shared", codeSectionCss());
|
|
821
|
+
}, []);
|
|
822
|
+
const { problems, validated, ops, secrets, unknown } = useCodeChecks(text, snapshot, t);
|
|
823
|
+
const ready = snapshot.status === "ready";
|
|
824
|
+
const disabled = !ready || !snapshot.writable || applying;
|
|
825
|
+
return {
|
|
826
|
+
ready,
|
|
827
|
+
ns,
|
|
828
|
+
t,
|
|
829
|
+
scope,
|
|
830
|
+
snapshot,
|
|
831
|
+
text,
|
|
832
|
+
setText,
|
|
833
|
+
status,
|
|
834
|
+
setStatus,
|
|
835
|
+
applying,
|
|
836
|
+
setApplying,
|
|
837
|
+
open,
|
|
838
|
+
setOpen,
|
|
839
|
+
problems,
|
|
840
|
+
validated,
|
|
841
|
+
ops,
|
|
842
|
+
secrets,
|
|
843
|
+
unknown,
|
|
844
|
+
disabled,
|
|
845
|
+
canApply: !disabled && problems.length === 0 && ops.length > 0,
|
|
846
|
+
...draftHandlers({
|
|
847
|
+
api,
|
|
848
|
+
scope,
|
|
849
|
+
t,
|
|
850
|
+
snapshot,
|
|
851
|
+
text,
|
|
852
|
+
ops,
|
|
853
|
+
setText,
|
|
854
|
+
setStatus,
|
|
855
|
+
setApplying
|
|
856
|
+
})
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
/** 提示行:无改动 / 未校验 / 密钥计数 / 模式外键 / 不可用或只读。 */
|
|
860
|
+
function noteLines(state) {
|
|
861
|
+
const { t, ready, problems, validated, ops, secrets, unknown, snapshot } = state;
|
|
862
|
+
const notes = [];
|
|
863
|
+
if (problems.length === 0 && ops.length === 0 && ready) notes.push(h$1("p", {
|
|
864
|
+
className: "dshc-hint",
|
|
865
|
+
key: "no-change"
|
|
866
|
+
}, t("codeNoChanges")));
|
|
867
|
+
if (problems.length === 0 && !validated) notes.push(h$1("p", {
|
|
868
|
+
className: "dshc-hint",
|
|
869
|
+
key: "unvalidated"
|
|
870
|
+
}, t("codeUnvalidated")));
|
|
871
|
+
if (secrets.length > 0) notes.push(h$1("p", {
|
|
872
|
+
className: "dshc-hint",
|
|
873
|
+
key: "secrets"
|
|
874
|
+
}, t("codeSecretsNote").replace("{n}", String(secrets.length))));
|
|
875
|
+
if (unknown.length > 0) notes.push(h$1("p", {
|
|
876
|
+
className: "dshc-hint",
|
|
877
|
+
key: "unknown"
|
|
878
|
+
}, t("codeUnknownKeys").replace("{n}", String(unknown.length)).replace("{keys}", unknown.slice(0, 8).join("、"))));
|
|
879
|
+
if (snapshot.status === "unavailable") notes.push(h$1("p", {
|
|
880
|
+
className: "dshc-hint",
|
|
881
|
+
key: "unavailable"
|
|
882
|
+
}, t("codeUnavailable")));
|
|
883
|
+
else if (!snapshot.writable) notes.push(h$1("p", {
|
|
884
|
+
className: "dshc-hint",
|
|
885
|
+
key: "readonly"
|
|
886
|
+
}, t("codeReadOnly")));
|
|
887
|
+
return notes;
|
|
888
|
+
}
|
|
889
|
+
/** 页脚:状态行 + 格式化 / 应用按钮。 */
|
|
890
|
+
function codeFooter(state) {
|
|
891
|
+
const { t, status, disabled, canApply, applying, onFormat, onApply } = state;
|
|
892
|
+
return h$1("div", { className: "dshc-foot" }, status.kind === "idle" ? null : h$1("p", {
|
|
893
|
+
className: `dshc-status${status.kind === "error" ? " dshc-statusError" : ""}`,
|
|
894
|
+
role: "status"
|
|
895
|
+
}, status.text), h$1("button", {
|
|
896
|
+
type: "button",
|
|
897
|
+
className: "dshc-btn dshc-btnGhost",
|
|
898
|
+
disabled,
|
|
899
|
+
onClick: onFormat
|
|
900
|
+
}, t("codeFormat")), h$1("button", {
|
|
901
|
+
type: "button",
|
|
902
|
+
className: "dshc-btn dshc-btnPrimary",
|
|
903
|
+
disabled: !canApply,
|
|
904
|
+
onClick: onApply
|
|
905
|
+
}, applying ? t("codeApplying") : t("codeApply")));
|
|
906
|
+
}
|
|
907
|
+
function ConfigCodeSection(props) {
|
|
908
|
+
const state = useCodeSection(props);
|
|
909
|
+
const { ns, t, text, disabled, open, setOpen, status, problems, onChange, onKeyDown } = state;
|
|
910
|
+
return h$1("section", {
|
|
911
|
+
className: "dshc-section",
|
|
912
|
+
"aria-label": `${ns} ${t("codeSectionTitle")}`
|
|
913
|
+
}, h$1("details", {
|
|
914
|
+
className: "dshc-details",
|
|
915
|
+
open: open || status.kind === "error",
|
|
916
|
+
onToggle: (event) => {
|
|
917
|
+
setOpen(event.currentTarget.open);
|
|
918
|
+
}
|
|
919
|
+
}, h$1("summary", { className: "dshc-summary" }, t("codeSectionTitle")), h$1("p", { className: "dshc-desc" }, t("codeSectionDesc")), h$1("textarea", {
|
|
920
|
+
className: `dshc-input${problems.length > 0 ? " dshc-inputInvalid" : ""}`,
|
|
921
|
+
rows: 16,
|
|
922
|
+
spellCheck: false,
|
|
923
|
+
"aria-invalid": problems.length > 0 ? true : void 0,
|
|
924
|
+
value: text,
|
|
925
|
+
disabled,
|
|
926
|
+
onChange,
|
|
927
|
+
onKeyDown
|
|
928
|
+
}), problems.map((problem) => h$1("p", {
|
|
929
|
+
className: "dshc-invalid",
|
|
930
|
+
role: "alert",
|
|
931
|
+
key: problem
|
|
932
|
+
}, problem)), ...noteLines(state), codeFooter(state)));
|
|
933
|
+
}
|
|
934
|
+
//#endregion
|
|
187
935
|
//#region ../shared/src/client/config-slots.ts
|
|
936
|
+
/**
|
|
937
|
+
* 配置卡片的槽位注册助手(目标宿主:dsh 0.2.1-alpha.2)。
|
|
938
|
+
*
|
|
939
|
+
* 官方插件页(`dsh-client-ui-plugin-manager`)为第三方配置视图声明两个 keyed 槽位:
|
|
940
|
+
* - `plugins.row.config`(key = `<bundle 包名>#<row id>`):bundle 详情页对应行出现
|
|
941
|
+
* 「配置」入口,行配置页渲染 `view: 'summary'`(一行简介)与 `view: 'page'`
|
|
942
|
+
* (带保存控件的表单);
|
|
943
|
+
* - `plugins.bundle.config`(key = 包名):包详情页本体配置,仅渲染 `view: 'page'`。
|
|
944
|
+
*
|
|
945
|
+
* 页面自己画标题、图标、面包屑与页面级内边距(`detailSections` 内无边框容器),
|
|
946
|
+
* 故 page 视图输出无边框分节(见 card.tsx),summary 视图只返回一行文本。
|
|
947
|
+
* 旧 `settings.plugin.item` 槽位已随官方插件页迁移删除,不再注册。
|
|
948
|
+
*
|
|
949
|
+
* **源码编辑节自动追加**:注册时把插件组件包一层,page 视图在原卡片之后追加
|
|
950
|
+
* `ConfigCodeSection`(复用同一份 scope/api),summary 视图原样透传。因此「只要
|
|
951
|
+
* 插件存在可配置项」就有源码编辑入口,插件侧零接线;命名空间没有 volatile 字段时
|
|
952
|
+
* 它本身不出现在 describe 视图里,该节自然显示为不可用。
|
|
953
|
+
*
|
|
954
|
+
* @module @dsh-plus/shared/client/config-slots
|
|
955
|
+
*/
|
|
956
|
+
const { createElement: h, Fragment } = react.default;
|
|
188
957
|
/** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
|
|
189
958
|
const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
|
|
959
|
+
/** page 视图:插件卡片 + 自动追加的源码编辑节(summary 视图原样透传)。 */
|
|
960
|
+
function withCodeSection(component, ns) {
|
|
961
|
+
return function AutoConfigView(props) {
|
|
962
|
+
const card = h(component, props);
|
|
963
|
+
if (props.view !== "page") return card;
|
|
964
|
+
return h(Fragment, null, card, h(ConfigCodeSection, {
|
|
965
|
+
ns,
|
|
966
|
+
t: props.t,
|
|
967
|
+
scope: props.scope,
|
|
968
|
+
api: props.api
|
|
969
|
+
}));
|
|
970
|
+
};
|
|
971
|
+
}
|
|
190
972
|
/**
|
|
191
973
|
* 把一张配置卡片注册进插件页的两个配置槽位。
|
|
192
974
|
* 两个槽位都由 0.2.1-alpha.2 的插件页声明,任一缺席即报错——不做版本探测。
|
|
193
975
|
*/
|
|
194
976
|
function injectPluginConfigCard(slots, reg) {
|
|
977
|
+
const component = withCodeSection(reg.component, reg.ns);
|
|
195
978
|
const base = {
|
|
196
979
|
locale: reg.ns,
|
|
197
980
|
inject: reg.inject
|
|
@@ -200,12 +983,112 @@ window.__ModuleLoader__.load({
|
|
|
200
983
|
name: "plugins.row.config",
|
|
201
984
|
key: `${DSH_PLUS_BUNDLE}#${reg.rowId}`,
|
|
202
985
|
...base
|
|
203
|
-
},
|
|
986
|
+
}, component));
|
|
204
987
|
slots.inject("plugins.bundle.config", () => slots.register({
|
|
205
988
|
name: "plugins.bundle.config",
|
|
206
989
|
key: reg.pkg,
|
|
207
990
|
...base
|
|
208
|
-
},
|
|
991
|
+
}, component));
|
|
992
|
+
}
|
|
993
|
+
//#endregion
|
|
994
|
+
//#region ../shared/src/client/credentials.tsx
|
|
995
|
+
/**
|
|
996
|
+
* 凭据面(共享件):官方 settings-controller 的 `remote.credentials` 命名空间
|
|
997
|
+
* (describe/set/unset/onChange),复用官方 credentials seam,不另开 HTTP 端点。
|
|
998
|
+
* **值只写不读**——describe 只回「已配置 / 来源 / 可写」,没有任何方法回传值。
|
|
999
|
+
*
|
|
1000
|
+
* 凭据与 settings 草稿相互独立:不进命名空间、不参与脏判定,保存即落
|
|
1001
|
+
* `$DSH_HOME/.credentials.yaml` 并热生效(宿主每次工具调用重新 resolve),
|
|
1002
|
+
* 因此改口令不需要保存卡片、也不需要 /reload 或重启。
|
|
1003
|
+
* @module @dsh-plus/shared/client/credentials
|
|
1004
|
+
*/
|
|
1005
|
+
/**
|
|
1006
|
+
* 把官方 `remote.credentials` 收成窄面:只补一个把宿主事件包成订阅的方法,
|
|
1007
|
+
* 让状态层不必认识 `$on` 的事件名与载荷形状。
|
|
1008
|
+
*/
|
|
1009
|
+
function credentialsFace(remote) {
|
|
1010
|
+
const { credentials } = remote;
|
|
1011
|
+
return {
|
|
1012
|
+
describe: (refs) => credentials.describe(refs),
|
|
1013
|
+
set: (ref, value) => credentials.set(ref, value),
|
|
1014
|
+
unset: (ref) => credentials.unset(ref),
|
|
1015
|
+
onChange: (listener) => remote.$on("credentials/reference-updated", (ref) => {
|
|
1016
|
+
if (typeof ref === "string") listener(ref);
|
|
1017
|
+
})
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/** 未答(或应答缺项)时的视图:按「未配置、可写」呈现。 */
|
|
1021
|
+
const ABSENT_VIEW = {
|
|
1022
|
+
configured: false,
|
|
1023
|
+
writable: true
|
|
1024
|
+
};
|
|
1025
|
+
/** 取某引用的视图;应答缺项回落为 {@link ABSENT_VIEW}。 */
|
|
1026
|
+
function viewOf(views, ref) {
|
|
1027
|
+
return views[ref] ?? ABSENT_VIEW;
|
|
1028
|
+
}
|
|
1029
|
+
/** 应答信封的错误文案(成功为空串)。 */
|
|
1030
|
+
function failureOf(result) {
|
|
1031
|
+
return result.ok ? "" : result.error.message ?? "";
|
|
1032
|
+
}
|
|
1033
|
+
function messageOf(error) {
|
|
1034
|
+
return error instanceof Error ? error.message : String(error);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* 凭据行状态:挂载即 describe 一次,写入后重新 describe;
|
|
1038
|
+
* 外部改动经 `credentials/reference-updated` 触发同一路径刷新。
|
|
1039
|
+
* @param face - 官方 credentials 远程面。
|
|
1040
|
+
* @param refs - 本卡片关心的引用名(以换行拼接作为稳定依赖,避免数组身份抖动)。
|
|
1041
|
+
*/
|
|
1042
|
+
function useCredentials$1(face, refs) {
|
|
1043
|
+
const key = refs.join("\n");
|
|
1044
|
+
const [views, setViews] = (0, react.useState)({});
|
|
1045
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1046
|
+
const [error, setError] = (0, react.useState)("");
|
|
1047
|
+
const read = (0, react.useCallback)(async () => {
|
|
1048
|
+
const list = key === "" ? [] : key.split("\n");
|
|
1049
|
+
const result = await face.describe(list);
|
|
1050
|
+
if (!result.ok) {
|
|
1051
|
+
setError(failureOf(result));
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
setError("");
|
|
1055
|
+
setViews(result.value);
|
|
1056
|
+
}, [face, key]);
|
|
1057
|
+
(0, react.useEffect)(() => {
|
|
1058
|
+
read();
|
|
1059
|
+
}, [read]);
|
|
1060
|
+
(0, react.useEffect)(() => face.onChange((ref) => {
|
|
1061
|
+
if ((key === "" ? [] : key.split("\n")).includes(ref)) read();
|
|
1062
|
+
}), [
|
|
1063
|
+
face,
|
|
1064
|
+
key,
|
|
1065
|
+
read
|
|
1066
|
+
]);
|
|
1067
|
+
const write = (0, react.useCallback)(async (run) => {
|
|
1068
|
+
setBusy(true);
|
|
1069
|
+
try {
|
|
1070
|
+
const result = await run();
|
|
1071
|
+
if (!result.ok) {
|
|
1072
|
+
setError(failureOf(result));
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
setError("");
|
|
1076
|
+
await read();
|
|
1077
|
+
return true;
|
|
1078
|
+
} catch (thrown) {
|
|
1079
|
+
setError(messageOf(thrown));
|
|
1080
|
+
return false;
|
|
1081
|
+
} finally {
|
|
1082
|
+
setBusy(false);
|
|
1083
|
+
}
|
|
1084
|
+
}, [read]);
|
|
1085
|
+
return {
|
|
1086
|
+
views,
|
|
1087
|
+
busy,
|
|
1088
|
+
error,
|
|
1089
|
+
save: (0, react.useCallback)((ref, value) => write(() => face.set(ref, value)), [face, write]),
|
|
1090
|
+
clear: (0, react.useCallback)((ref) => write(() => face.unset(ref)), [face, write])
|
|
1091
|
+
};
|
|
209
1092
|
}
|
|
210
1093
|
//#endregion
|
|
211
1094
|
//#region ../shared/src/client/draft.ts
|
|
@@ -302,7 +1185,31 @@ window.__ModuleLoader__.load({
|
|
|
302
1185
|
enabledOff: "未启用",
|
|
303
1186
|
saveFailed: "保存失败,请检查填写内容。",
|
|
304
1187
|
confirm: "确认",
|
|
305
|
-
cancel: "取消"
|
|
1188
|
+
cancel: "取消",
|
|
1189
|
+
listAdd: "新增",
|
|
1190
|
+
listRemove: "删除",
|
|
1191
|
+
listMoveUp: "上移",
|
|
1192
|
+
listMoveDown: "下移",
|
|
1193
|
+
listDuplicate: "复制",
|
|
1194
|
+
listEmpty: "暂无条目。",
|
|
1195
|
+
codeSectionTitle: "源码编辑(JSON)",
|
|
1196
|
+
codeSectionDesc: "直接编辑本插件的可配置字段(仅 .volatile() 字段)。「应用」按官方路径编辑写回用户层,密钥字段不在此编辑器内。",
|
|
1197
|
+
codeFormat: "格式化",
|
|
1198
|
+
codeApply: "应用",
|
|
1199
|
+
codeApplying: "应用中…",
|
|
1200
|
+
codeApplied: "已应用。",
|
|
1201
|
+
codeNoChanges: "与当前配置一致,无需应用。",
|
|
1202
|
+
codeInvalidJson: "JSON 语法错误",
|
|
1203
|
+
codeInvalidEmpty: "配置不能为空。",
|
|
1204
|
+
codeInvalidRoot: "根节点必须是对象。",
|
|
1205
|
+
codeInvalidSchema: "模式校验未通过",
|
|
1206
|
+
codeUnvalidated: "宿主未回传 schema,已跳过本地校验(写入仍由宿主把关)。",
|
|
1207
|
+
codeSecretsNote: "有 {n} 个密钥字段不在此编辑器内(在凭据区或行级配置中管理)。",
|
|
1208
|
+
codeUnknownKeys: "有 {n} 个键不在 schema 里({keys}):会被原样保留并被运行时忽略,建议删除。",
|
|
1209
|
+
codeUnavailable: "该行未激活或命名空间不可用,源码编辑不可用。",
|
|
1210
|
+
codeReadOnly: "当前部署无 settings provider,源码编辑只读。",
|
|
1211
|
+
codeApplyFailed: "应用失败:",
|
|
1212
|
+
codeLine: "第 {line} 行第 {column} 列"
|
|
306
1213
|
};
|
|
307
1214
|
const commonEn = {
|
|
308
1215
|
save: "Save",
|
|
@@ -319,7 +1226,31 @@ window.__ModuleLoader__.load({
|
|
|
319
1226
|
enabledOff: "Off",
|
|
320
1227
|
saveFailed: "Save failed; check the fields.",
|
|
321
1228
|
confirm: "Confirm",
|
|
322
|
-
cancel: "Cancel"
|
|
1229
|
+
cancel: "Cancel",
|
|
1230
|
+
listAdd: "Add",
|
|
1231
|
+
listRemove: "Remove",
|
|
1232
|
+
listMoveUp: "Move up",
|
|
1233
|
+
listMoveDown: "Move down",
|
|
1234
|
+
listDuplicate: "Duplicate",
|
|
1235
|
+
listEmpty: "No entries yet.",
|
|
1236
|
+
codeSectionTitle: "Source editor (JSON)",
|
|
1237
|
+
codeSectionDesc: "Edit this plugin’s configurable fields directly (only .volatile() fields). Apply writes back through the official path edits; secret fields never appear here.",
|
|
1238
|
+
codeFormat: "Format",
|
|
1239
|
+
codeApply: "Apply",
|
|
1240
|
+
codeApplying: "Applying…",
|
|
1241
|
+
codeApplied: "Applied.",
|
|
1242
|
+
codeNoChanges: "Identical to the current config; nothing to apply.",
|
|
1243
|
+
codeInvalidJson: "JSON syntax error",
|
|
1244
|
+
codeInvalidEmpty: "Config must not be empty.",
|
|
1245
|
+
codeInvalidRoot: "The root node must be an object.",
|
|
1246
|
+
codeInvalidSchema: "Schema validation failed",
|
|
1247
|
+
codeUnvalidated: "The host returned no schema, so local validation was skipped (writes are still validated by the host).",
|
|
1248
|
+
codeSecretsNote: "{n} secret field(s) are not editable here (manage them in the credentials section or at the row level).",
|
|
1249
|
+
codeUnknownKeys: "{n} key(s) are absent from the schema ({keys}): they are preserved verbatim and ignored at runtime; remove them.",
|
|
1250
|
+
codeUnavailable: "This row is inactive or its namespace is unavailable; the source editor is disabled.",
|
|
1251
|
+
codeReadOnly: "No settings provider in this deployment; the source editor is read-only.",
|
|
1252
|
+
codeApplyFailed: "Apply failed: ",
|
|
1253
|
+
codeLine: "line {line}, column {column}"
|
|
323
1254
|
};
|
|
324
1255
|
/**
|
|
325
1256
|
* own 覆盖同键合并(返回新对象,入参不可变)。
|
|
@@ -346,6 +1277,8 @@ window.__ModuleLoader__.load({
|
|
|
346
1277
|
let state = {
|
|
347
1278
|
status: "loading",
|
|
348
1279
|
value: void 0,
|
|
1280
|
+
schema: void 0,
|
|
1281
|
+
secrets: [],
|
|
349
1282
|
revision: void 0,
|
|
350
1283
|
writable: false
|
|
351
1284
|
};
|
|
@@ -371,6 +1304,8 @@ window.__ModuleLoader__.load({
|
|
|
371
1304
|
publish({
|
|
372
1305
|
status: "unavailable",
|
|
373
1306
|
value: void 0,
|
|
1307
|
+
schema: void 0,
|
|
1308
|
+
secrets: [],
|
|
374
1309
|
revision: void 0,
|
|
375
1310
|
writable: view.writable
|
|
376
1311
|
});
|
|
@@ -379,6 +1314,8 @@ window.__ModuleLoader__.load({
|
|
|
379
1314
|
publish({
|
|
380
1315
|
status: "ready",
|
|
381
1316
|
value: namespace.value,
|
|
1317
|
+
schema: namespace.schema,
|
|
1318
|
+
secrets: namespace.secrets,
|
|
382
1319
|
revision: namespace.revision,
|
|
383
1320
|
writable: view.writable
|
|
384
1321
|
});
|
|
@@ -412,6 +1349,10 @@ window.__ModuleLoader__.load({
|
|
|
412
1349
|
const response = await remote.update(ns, patch, expectedRevision);
|
|
413
1350
|
if (!response.ok) throw new Error(response.error.message ?? "settings update failed");
|
|
414
1351
|
},
|
|
1352
|
+
mutate: async (ops, expectedRevision) => {
|
|
1353
|
+
const response = await remote.mutate(ns, ops, expectedRevision);
|
|
1354
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings mutate failed");
|
|
1355
|
+
},
|
|
415
1356
|
describe: async () => {
|
|
416
1357
|
const response = await remote.describe();
|
|
417
1358
|
if (!response.ok) throw new Error(response.error.message ?? "settings describe failed");
|
|
@@ -420,101 +1361,6 @@ window.__ModuleLoader__.load({
|
|
|
420
1361
|
};
|
|
421
1362
|
}
|
|
422
1363
|
//#endregion
|
|
423
|
-
//#region ../shared/src/client/styles.ts
|
|
424
|
-
/**
|
|
425
|
-
* 配置分节样式工厂(各插件 client 的公共收编版)。
|
|
426
|
-
*
|
|
427
|
-
* 目标宿主 dsh 0.2.1-alpha.2:插件页把配置视图放进已有页面级内边距的容器里
|
|
428
|
-
* (`div.detailSections[data-plugin-config]` 或 `section.detailSection[data-plugin-config]`),
|
|
429
|
-
* 外层无边框——故这里输出的是**无边框分节**(小节标题 13px/600、字段 12px 0、
|
|
430
|
-
* 横向零内边距、页脚非 sticky),度量对齐官方设置页
|
|
431
|
-
* (`@deepseek-ai/dsh-client-ui-primitives` 的 settings-form)。
|
|
432
|
-
*
|
|
433
|
-
* 移动端与可达性(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
|
|
434
|
-
* - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
|
|
435
|
-
* - 页脚按钮等宽换行、状态文字独占一行。
|
|
436
|
-
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
|
|
437
|
-
* @module @dsh-plus/shared/client/styles
|
|
438
|
-
*/
|
|
439
|
-
/** 生成全套分节 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
|
|
440
|
-
function cardCss(prefix, extra = "") {
|
|
441
|
-
return `
|
|
442
|
-
.${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
|
|
443
|
-
.${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
444
|
-
.${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
|
|
445
|
-
.${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
446
|
-
.${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
|
|
447
|
-
.${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
|
|
448
|
-
.${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
|
|
449
|
-
.${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}
|
|
450
|
-
.${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
451
|
-
.${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
|
|
452
|
-
.${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
453
|
-
.${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
454
|
-
.${prefix}-head{align-items:center;gap:8px;display:flex}
|
|
455
|
-
.${prefix}-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
456
|
-
.${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
457
|
-
.${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
458
|
-
.${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
|
|
459
|
-
.${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}
|
|
460
|
-
.${prefix}-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
|
|
461
|
-
.${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
462
|
-
.${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
463
|
-
.${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
464
|
-
.${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}
|
|
465
|
-
.${prefix}-select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}
|
|
466
|
-
.${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
467
|
-
.${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
468
|
-
.${prefix}-checkRow{align-items:center;gap:10px;display:flex;padding:6px 0}
|
|
469
|
-
.${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer;flex:1;min-width:0}
|
|
470
|
-
/* 自绘圆角扁平开关(官方 UI 无可见原生 checkbox,选择类全走胶囊/菜单) */
|
|
471
|
-
.${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}
|
|
472
|
-
.${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)}
|
|
473
|
-
.${prefix}-checkRow input:checked{background:var(--dsw-alias-brand-primary)}
|
|
474
|
-
.${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
|
|
475
|
-
.${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
|
|
476
|
-
.${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
477
|
-
.${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
478
|
-
.${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
479
|
-
.${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
480
|
-
.${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
|
|
481
|
-
.${prefix}-statusError{color:var(--dsw-alias-label-error)}
|
|
482
|
-
.${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}
|
|
483
|
-
.${prefix}-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
|
|
484
|
-
.${prefix}-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
|
|
485
|
-
.${prefix}-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
486
|
-
.${prefix}-btn:disabled{opacity:.4;cursor:default}
|
|
487
|
-
.${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
488
|
-
@media (max-width:767px){
|
|
489
|
-
.${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
|
|
490
|
-
.${prefix}-checkRow{min-height:44px;padding:8px 0}
|
|
491
|
-
.${prefix}-status{flex-basis:100%;flex:none}
|
|
492
|
-
.${prefix}-btn{min-height:44px;flex:1;min-width:96px}
|
|
493
|
-
}
|
|
494
|
-
@media (pointer:coarse){
|
|
495
|
-
.${prefix}-btn{min-height:44px}
|
|
496
|
-
.${prefix}-checkRow{min-height:44px}
|
|
497
|
-
.${prefix}-input,.${prefix}-select{height:40px}
|
|
498
|
-
}
|
|
499
|
-
${extra}
|
|
500
|
-
`;
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* 幂等注入样式标签(官方 data-plugin-css 约定);返回新标签,
|
|
504
|
-
* 已存在或环境无 document 时返回 null(调用方 effect 卸载用)。
|
|
505
|
-
*/
|
|
506
|
-
function injectCardStyle(pluginId, css) {
|
|
507
|
-
if (typeof document === "undefined") return null;
|
|
508
|
-
const tagId = `${pluginId}/card.css`;
|
|
509
|
-
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) !== null) return null;
|
|
510
|
-
const tag = document.createElement("style");
|
|
511
|
-
tag.dataset.plugin = pluginId;
|
|
512
|
-
tag.dataset.pluginCss = tagId;
|
|
513
|
-
tag.textContent = css;
|
|
514
|
-
document.head.appendChild(tag);
|
|
515
|
-
return tag;
|
|
516
|
-
}
|
|
517
|
-
//#endregion
|
|
518
1364
|
//#region src/ns.ts
|
|
519
1365
|
/**
|
|
520
1366
|
* settings 命名空间字面量(纯常量,零依赖)。
|
|
@@ -539,7 +1385,6 @@ ${extra}
|
|
|
539
1385
|
/** 命名空间解析值 → 草稿。 */
|
|
540
1386
|
function draftFromValue(value) {
|
|
541
1387
|
return {
|
|
542
|
-
enabled: value.enabled,
|
|
543
1388
|
serverUrl: value.serverUrl,
|
|
544
1389
|
syncId: value.syncId,
|
|
545
1390
|
cliCommand: argvToText(value.cliCommand),
|
|
@@ -552,7 +1397,6 @@ ${extra}
|
|
|
552
1397
|
/** 草稿 → 命名空间补丁(仅本卡片负责的字段)。 */
|
|
553
1398
|
function patchFromDraft(draft) {
|
|
554
1399
|
return {
|
|
555
|
-
enabled: draft.enabled,
|
|
556
1400
|
serverUrl: draft.serverUrl.trim(),
|
|
557
1401
|
syncId: draft.syncId.trim(),
|
|
558
1402
|
cliCommand: textToArgv(draft.cliCommand),
|
|
@@ -606,78 +1450,18 @@ ${extra}
|
|
|
606
1450
|
];
|
|
607
1451
|
//#endregion
|
|
608
1452
|
//#region src/client/secrets.ts
|
|
609
|
-
/** 未答(或应答缺项)时的视图:按「未配置、可写」呈现。 */
|
|
610
|
-
const ABSENT_VIEW = {
|
|
611
|
-
configured: false,
|
|
612
|
-
writable: true
|
|
613
|
-
};
|
|
614
|
-
/** 取某引用的视图;应答缺项回落为 {@link ABSENT_VIEW}。 */
|
|
615
|
-
function viewOf(views, ref) {
|
|
616
|
-
return views[ref] ?? ABSENT_VIEW;
|
|
617
|
-
}
|
|
618
|
-
/** 应答信封的错误文案(成功为空串)。 */
|
|
619
|
-
function failureOf(result) {
|
|
620
|
-
return result.ok ? "" : result.error.message ?? "";
|
|
621
|
-
}
|
|
622
|
-
/** 提取异常文本。 */
|
|
623
|
-
function messageOf(error) {
|
|
624
|
-
return error instanceof Error ? error.message : String(error);
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* 订阅宿主凭据变更:只认本插件关心的引用,其余(别的插件写自己的密钥)
|
|
628
|
-
* 不触发重读。
|
|
629
|
-
*/
|
|
630
|
-
function useCredentialChanges(face, read) {
|
|
631
|
-
(0, react.useEffect)(() => face.onChange((ref) => {
|
|
632
|
-
if (CREDENTIAL_REFS.includes(ref)) read();
|
|
633
|
-
}), [face, read]);
|
|
634
|
-
}
|
|
635
1453
|
/**
|
|
636
|
-
*
|
|
637
|
-
*
|
|
1454
|
+
* 浏览器半的凭据面:复用 `@dsh-plus/shared/client` 的凭据 hook(describe/set/unset,
|
|
1455
|
+
* 支持多引用),此处只绑定本插件关心的引用清单。
|
|
1456
|
+
*
|
|
1457
|
+
* **值只写不读**——describe 只回「已配置 / 来源 / 可写」,没有任何方法回传值。
|
|
1458
|
+
* 凭据与 settings 草稿相互独立:它不进命名空间、不参与脏判定,保存即落
|
|
1459
|
+
* `$DSH_HOME/.credentials.yaml` 并热生效(宿主每次工具调用重新 resolve)。
|
|
1460
|
+
* @module @dsh-plus/actual/client/secrets
|
|
638
1461
|
*/
|
|
1462
|
+
/** 本插件的凭据行状态(引用清单固定为 {@link CREDENTIAL_REFS})。 */
|
|
639
1463
|
function useCredentials(face) {
|
|
640
|
-
|
|
641
|
-
const [busy, setBusy] = (0, react.useState)(false);
|
|
642
|
-
const [error, setError] = (0, react.useState)("");
|
|
643
|
-
const read = (0, react.useCallback)(async () => {
|
|
644
|
-
const result = await face.describe([...CREDENTIAL_REFS]);
|
|
645
|
-
if (!result.ok) {
|
|
646
|
-
setError(failureOf(result));
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
setError("");
|
|
650
|
-
setViews(result.value);
|
|
651
|
-
}, [face]);
|
|
652
|
-
(0, react.useEffect)(() => {
|
|
653
|
-
read();
|
|
654
|
-
}, [read]);
|
|
655
|
-
useCredentialChanges(face, read);
|
|
656
|
-
const write = (0, react.useCallback)(async (run) => {
|
|
657
|
-
setBusy(true);
|
|
658
|
-
try {
|
|
659
|
-
const result = await run();
|
|
660
|
-
if (!result.ok) {
|
|
661
|
-
setError(failureOf(result));
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
setError("");
|
|
665
|
-
await read();
|
|
666
|
-
return true;
|
|
667
|
-
} catch (thrown) {
|
|
668
|
-
setError(messageOf(thrown));
|
|
669
|
-
return false;
|
|
670
|
-
} finally {
|
|
671
|
-
setBusy(false);
|
|
672
|
-
}
|
|
673
|
-
}, [read]);
|
|
674
|
-
return {
|
|
675
|
-
views,
|
|
676
|
-
busy,
|
|
677
|
-
error,
|
|
678
|
-
save: (0, react.useCallback)((ref, value) => write(() => face.set(ref, value)), [face, write]),
|
|
679
|
-
clear: (0, react.useCallback)((ref) => write(() => face.unset(ref)), [face, write])
|
|
680
|
-
};
|
|
1464
|
+
return useCredentials$1(face, CREDENTIAL_REFS);
|
|
681
1465
|
}
|
|
682
1466
|
//#endregion
|
|
683
1467
|
//#region src/client/secrets-section.tsx
|
|
@@ -826,19 +1610,6 @@ ${extra}
|
|
|
826
1610
|
invalidLabel: label
|
|
827
1611
|
} : { invalid: false };
|
|
828
1612
|
}
|
|
829
|
-
/** 说明段落(无编辑器的字段在此交代落点)。 */
|
|
830
|
-
function Note(props) {
|
|
831
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
832
|
-
className: "act-note",
|
|
833
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
834
|
-
className: "act-noteTitle",
|
|
835
|
-
children: props.title
|
|
836
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
837
|
-
className: "act-noteBody",
|
|
838
|
-
children: props.body
|
|
839
|
-
})]
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
1613
|
/** 连接小节:服务器地址、预算、CLI 命令与版本策略。 */
|
|
843
1614
|
function ConnectionSection(props) {
|
|
844
1615
|
const { t, draft, disabled, violation, onEdit } = props;
|
|
@@ -903,7 +1674,7 @@ ${extra}
|
|
|
903
1674
|
onEdit: (value) => onEdit({ confirmWrites: value })
|
|
904
1675
|
}),
|
|
905
1676
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
906
|
-
className: "act-
|
|
1677
|
+
className: "act-hint",
|
|
907
1678
|
children: t("confirmWritesHint")
|
|
908
1679
|
}),
|
|
909
1680
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
@@ -915,7 +1686,7 @@ ${extra}
|
|
|
915
1686
|
onEdit: (value) => onEdit({ auxTools: value })
|
|
916
1687
|
}),
|
|
917
1688
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
918
|
-
className: "act-
|
|
1689
|
+
className: "act-hint",
|
|
919
1690
|
children: t("auxToolsHint")
|
|
920
1691
|
}),
|
|
921
1692
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
@@ -952,14 +1723,10 @@ ${extra}
|
|
|
952
1723
|
violation,
|
|
953
1724
|
onEdit: edit
|
|
954
1725
|
};
|
|
955
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
1726
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ConfigShell, {
|
|
956
1727
|
prefix: "act",
|
|
957
1728
|
title: t("title"),
|
|
958
1729
|
description: t("description"),
|
|
959
|
-
statusBadge: {
|
|
960
|
-
text: t(draft.enabled ? "enabledOn" : "enabledOff"),
|
|
961
|
-
on: draft.enabled
|
|
962
|
-
},
|
|
963
1730
|
dirty,
|
|
964
1731
|
dirtyLabel: t("unsaved"),
|
|
965
1732
|
readOnlyNotice: disabled ? t("readOnly") : void 0,
|
|
@@ -977,18 +1744,6 @@ ${extra}
|
|
|
977
1744
|
onClick: save
|
|
978
1745
|
}],
|
|
979
1746
|
children: [
|
|
980
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
981
|
-
prefix: "act",
|
|
982
|
-
id: "act-enabled",
|
|
983
|
-
label: t("enabled"),
|
|
984
|
-
checked: draft.enabled,
|
|
985
|
-
disabled,
|
|
986
|
-
onEdit: (value) => edit({ enabled: value })
|
|
987
|
-
}),
|
|
988
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
989
|
-
className: "act-noteBody",
|
|
990
|
-
children: t("enabledHint")
|
|
991
|
-
}),
|
|
992
1747
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConnectionSection, { ...section }),
|
|
993
1748
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretsSection, {
|
|
994
1749
|
t,
|
|
@@ -996,6 +1751,7 @@ ${extra}
|
|
|
996
1751
|
}),
|
|
997
1752
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PolicySection, { ...section }),
|
|
998
1753
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Note, {
|
|
1754
|
+
prefix: "act",
|
|
999
1755
|
title: t("advancedTitle"),
|
|
1000
1756
|
body: t("advancedBody")
|
|
1001
1757
|
})
|
|
@@ -1017,8 +1773,6 @@ ${extra}
|
|
|
1017
1773
|
title: "Actual Budget 连接",
|
|
1018
1774
|
description: "配置本机 Actual 同步服务器与 CLI 行为。改动热生效,无需重载:连接相关字段会触发能力重新发现。",
|
|
1019
1775
|
summaryLine: "Actual 服务器地址、预算 Sync ID、CLI 与工具策略。",
|
|
1020
|
-
enabled: "启用 actual 预设",
|
|
1021
|
-
enabledHint: "关闭即不注册该预设(等价插件未安装);已开的会话保持原组合,新会话起不再出现。",
|
|
1022
1776
|
serverUrl: "同步服务器地址",
|
|
1023
1777
|
serverUrlHint: "自托管默认 http://127.0.0.1:5006。服务端版本从 <地址>/info 读取,用于版本一致性告警。",
|
|
1024
1778
|
serverUrlInvalid: "必须是 http(s):// 开头的完整地址",
|
|
@@ -1058,8 +1812,6 @@ ${extra}
|
|
|
1058
1812
|
title: "Actual Budget connection",
|
|
1059
1813
|
description: "Configure the local Actual sync server and CLI behaviour. Changes apply hot — connection fields trigger a fresh capability discovery.",
|
|
1060
1814
|
summaryLine: "Actual server address, budget sync ID, CLI and tool policy.",
|
|
1061
|
-
enabled: "Enable the actual preset",
|
|
1062
|
-
enabledHint: "Off stops registering the preset (equivalent to not installed). Live sessions keep their composition; new ones will not see it.",
|
|
1063
1815
|
serverUrl: "Sync server URL",
|
|
1064
1816
|
serverUrlHint: "Self-hosted default is http://127.0.0.1:5006. The server version is read from <url>/info for the version-mismatch warning.",
|
|
1065
1817
|
serverUrlInvalid: "Must be a full http(s):// URL",
|
|
@@ -1132,22 +1884,6 @@ ${extra}
|
|
|
1132
1884
|
"remote.settings",
|
|
1133
1885
|
"remote.credentials"
|
|
1134
1886
|
];
|
|
1135
|
-
/**
|
|
1136
|
-
* 把官方 `remote.credentials` 收成窄面:只补一个把宿主事件包成订阅的方法,
|
|
1137
|
-
* 让状态层不必认识 `$on` 的事件名与载荷形状。
|
|
1138
|
-
*/
|
|
1139
|
-
function credentialsFace(c) {
|
|
1140
|
-
const remote = c.get("remote");
|
|
1141
|
-
const { credentials } = remote;
|
|
1142
|
-
return {
|
|
1143
|
-
describe: (refs) => credentials.describe(refs),
|
|
1144
|
-
set: (ref, value) => credentials.set(ref, value),
|
|
1145
|
-
unset: (ref) => credentials.unset(ref),
|
|
1146
|
-
onChange: (listener) => remote.$on("credentials/reference-updated", (ref) => {
|
|
1147
|
-
if (typeof ref === "string") listener(ref);
|
|
1148
|
-
})
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
1887
|
function apply(ctx) {
|
|
1152
1888
|
const c = ctx;
|
|
1153
1889
|
const tag = injectStyle();
|
|
@@ -1160,7 +1896,7 @@ ${extra}
|
|
|
1160
1896
|
}), "actual: locale");
|
|
1161
1897
|
const scope = createSettingsScope(c, SETTINGS_NS, "actual: settings scope");
|
|
1162
1898
|
const api = createNamespaceApi(c.get("remote").settings, SETTINGS_NS);
|
|
1163
|
-
const credentials = credentialsFace(c);
|
|
1899
|
+
const credentials = credentialsFace(c.get("remote"));
|
|
1164
1900
|
injectPluginConfigCard(c.slots, {
|
|
1165
1901
|
ns: SETTINGS_NS,
|
|
1166
1902
|
rowId: "dsh-plus-actual",
|