@xlight-oss/visionary-dsh 0.7.1 → 0.7.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/README.md +15 -6
- package/cordis.patch.yml +7 -6
- package/lib/client.js +814 -0
- package/lib/image-bridge/core.mjs +22 -6
- package/lib/image-bridge/index.mjs +91 -85
- package/lib/index.mjs +27 -10
- package/package.json +23 -16
- package/lib/image-bridge/trust-fence.mjs +0 -85
- package/lib/settings-card/client.js +0 -607
- package/lib/settings-card/index.mjs +0 -18
- package/lib/settings-card/package.json +0 -36
- package/lib/settings-route.mjs +0 -215
package/lib/client.js
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
// Browser half of the visionary-dsh plugin.
|
|
2
|
+
//
|
|
3
|
+
// One native `settings.plugin.item` card per settings namespace the host
|
|
4
|
+
// serves (`visionary-vision`, `visionary-image-bridge`). The Plugin
|
|
5
|
+
// configuration tab dispatches that keyed slot by namespace and pairs the
|
|
6
|
+
// host-served namespace with the card registered under the same key, so the
|
|
7
|
+
// cards appear under Settings → Plugins → Plugin configuration.
|
|
8
|
+
//
|
|
9
|
+
// Transport: `ctx.settingsScope.bind({ namespace })` — the host's own settings
|
|
10
|
+
// channel. `getSnapshot()` drives rendering, `subscribe()` repaints, and writes
|
|
11
|
+
// go through `mutate(ops, revision)` with the revision read from the snapshot
|
|
12
|
+
// as the fence. There is no plugin-owned HTTP route and no snapshot-generation
|
|
13
|
+
// layer: the scope owns the wire, the revision fence, and the recovery read.
|
|
14
|
+
//
|
|
15
|
+
// Hand-written in the lazy-CJS bundle protocol (`window.__ModuleLoader__.load`)
|
|
16
|
+
// because no published preset emits that artifact for out-of-repo packages.
|
|
17
|
+
//
|
|
18
|
+
// Discovery contract (dsh-client-modules): a browser bundle is served only for
|
|
19
|
+
// a Loader row whose *bare package specifier* (e.g. `@xlight-oss/visionary-dsh`)
|
|
20
|
+
// resolves to a manifest declaring `dsh.client` + `exports["./client"]` — a
|
|
21
|
+
// subpath row name like `@xlight-oss/visionary-dsh/settings-card` is rejected
|
|
22
|
+
// by exactPackageSpecifier and silently yields no entry. The registered id must
|
|
23
|
+
// equal that package name, because the runner activates the row by requiring
|
|
24
|
+
// it. Hence this bundle lives at `lib/client.js` of the main package and is
|
|
25
|
+
// discovered through the main (vision) Loader row — no extra row is needed.
|
|
26
|
+
// Module edges: `react` only (a platform seed word). Service dependencies are
|
|
27
|
+
// waited on through cordis `inject`, not through module edges.
|
|
28
|
+
window.__ModuleLoader__.load({
|
|
29
|
+
id: "@xlight-oss/visionary-dsh",
|
|
30
|
+
factory: (require) => {
|
|
31
|
+
var module = { exports: {} };
|
|
32
|
+
var exports = module.exports;
|
|
33
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
34
|
+
var React = require("react");
|
|
35
|
+
// The host's UI primitives are a platform seed module (every shipped card
|
|
36
|
+
// requires them), so the Switch below is the deployment's own control rather
|
|
37
|
+
// than a re-invented one. The default-checkbox fallback keeps the card usable
|
|
38
|
+
// if a future shell stops exposing the seed word.
|
|
39
|
+
var primitives = {};
|
|
40
|
+
try {
|
|
41
|
+
primitives = require("@deepseek-ai/dsh-client-ui-primitives") || {};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn("visionary-dsh: UI primitives unavailable, falling back to plain controls", error);
|
|
44
|
+
}
|
|
45
|
+
var SwitchControl = typeof primitives.Switch === "function"
|
|
46
|
+
? primitives.Switch
|
|
47
|
+
: function FallbackSwitch(props) {
|
|
48
|
+
return React.createElement("input", {
|
|
49
|
+
type: "checkbox",
|
|
50
|
+
checked: Boolean(props.checked),
|
|
51
|
+
disabled: Boolean(props.disabled),
|
|
52
|
+
"aria-label": props.label,
|
|
53
|
+
onChange: function (event) { props.onChange(event.target.checked); },
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
var NS_VISION = "visionary-vision";
|
|
59
|
+
var NS_BRIDGE = "visionary-image-bridge";
|
|
60
|
+
|
|
61
|
+
// ── field definitions ──
|
|
62
|
+
// `area` decides the card region: "main" is always visible, "advanced"
|
|
63
|
+
// lives behind the collapsed disclosure. `targets` lists the namespace keys
|
|
64
|
+
// this field writes — one entry for a normal field, two for the shared
|
|
65
|
+
// `binaryPath` (the tools and the bridge both resolve the binary).
|
|
66
|
+
|
|
67
|
+
function field(ns, key, kind, area, opts) {
|
|
68
|
+
var base = {
|
|
69
|
+
id: ns + "." + key,
|
|
70
|
+
key: key,
|
|
71
|
+
kind: kind,
|
|
72
|
+
area: area,
|
|
73
|
+
targets: [{ ns: ns, key: key }],
|
|
74
|
+
};
|
|
75
|
+
for (var k in opts) if (Object.prototype.hasOwnProperty.call(opts, k)) base[k] = opts[k];
|
|
76
|
+
return base;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var VISION_FIELDS = [
|
|
80
|
+
field(NS_VISION, "modelType", "select", "main", {
|
|
81
|
+
options: ["vision", "ocr"],
|
|
82
|
+
labelKey: "visionModelTypeLabel",
|
|
83
|
+
hintKey: "visionModelTypeHint",
|
|
84
|
+
}),
|
|
85
|
+
field(NS_VISION, "visionTimeoutMs", "number", "main", {
|
|
86
|
+
labelKey: "visionTimeoutLabel",
|
|
87
|
+
hintKey: "visionTimeoutHint",
|
|
88
|
+
}),
|
|
89
|
+
field(NS_VISION, "binaryPath", "text", "main", {
|
|
90
|
+
labelKey: "sharedBinaryPathLabel",
|
|
91
|
+
hintKey: "sharedBinaryPathHint",
|
|
92
|
+
targets: [
|
|
93
|
+
{ ns: NS_VISION, key: "binaryPath" },
|
|
94
|
+
{ ns: NS_BRIDGE, key: "binaryPath" },
|
|
95
|
+
],
|
|
96
|
+
}),
|
|
97
|
+
field(NS_VISION, "statusTimeoutMs", "number", "advanced", {
|
|
98
|
+
labelKey: "visionStatusTimeoutLabel",
|
|
99
|
+
hintKey: "visionStatusTimeoutHint",
|
|
100
|
+
}),
|
|
101
|
+
field(NS_VISION, "loginTimeoutSeconds", "number", "advanced", {
|
|
102
|
+
labelKey: "visionLoginTimeoutLabel",
|
|
103
|
+
hintKey: "visionLoginTimeoutHint",
|
|
104
|
+
}),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
var BRIDGE_FIELDS = [
|
|
108
|
+
field(NS_BRIDGE, "enabled", "boolean", "main", {
|
|
109
|
+
labelKey: "enabledLabel",
|
|
110
|
+
hintKey: "enabledHint",
|
|
111
|
+
}),
|
|
112
|
+
field(NS_BRIDGE, "scope", "select", "main", {
|
|
113
|
+
options: ["text-only", "also-vl"],
|
|
114
|
+
labelKey: "scopeLabel",
|
|
115
|
+
hintKey: "scopeHint",
|
|
116
|
+
}),
|
|
117
|
+
field(NS_BRIDGE, "mode", "select", "main", {
|
|
118
|
+
options: ["agentic", "deterministic"],
|
|
119
|
+
labelKey: "modeLabel",
|
|
120
|
+
hintKey: "modeHint",
|
|
121
|
+
}),
|
|
122
|
+
field(NS_BRIDGE, "promptTemplate", "textarea", "advanced", {
|
|
123
|
+
labelKey: "promptTemplateLabel",
|
|
124
|
+
hintKey: "promptTemplateHint",
|
|
125
|
+
placeholderKey: "promptTemplateHint",
|
|
126
|
+
}),
|
|
127
|
+
field(NS_BRIDGE, "pastedDir", "text", "advanced", {
|
|
128
|
+
labelKey: "pastedDirLabel",
|
|
129
|
+
hintKey: "pastedDirHint",
|
|
130
|
+
}),
|
|
131
|
+
field(NS_BRIDGE, "retainHours", "number", "advanced", {
|
|
132
|
+
labelKey: "retainHoursLabel",
|
|
133
|
+
hintKey: "retainHoursHint",
|
|
134
|
+
}),
|
|
135
|
+
field(NS_BRIDGE, "cleanPasted", "trigger", "advanced", {
|
|
136
|
+
labelKey: "cleanPastedLabel",
|
|
137
|
+
hintKey: "cleanPastedHint",
|
|
138
|
+
actionKey: "cleanPastedAction",
|
|
139
|
+
}),
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
// ── locale ──
|
|
143
|
+
|
|
144
|
+
var NS = "settings.plugins.visionary";
|
|
145
|
+
|
|
146
|
+
var LOCALE_ZH = {
|
|
147
|
+
visionTitle: "Visionary · 视觉工具",
|
|
148
|
+
visionDescription: "deepseek_vision / deepseek_ocr 等 5 个原生工具:上传管道、超时与二进制路径,修改即时生效",
|
|
149
|
+
bridgeTitle: "Visionary · 图片桥接",
|
|
150
|
+
bridgeDescription: "纯文本模型粘贴图片时自动放行并改写为文本引导;VL 模型默认不受干预",
|
|
151
|
+
advanced: "高级",
|
|
152
|
+
advancedHint: "低频选项:超时、落盘与模板",
|
|
153
|
+
visionModelTypeLabel: "上传管道",
|
|
154
|
+
visionModelTypeHint: "vision(默认):完整多模态理解 | ocr:deepseek_vision 走纯文字提取管道,等价每次调用 deepseek_ocr。修改后即时生效",
|
|
155
|
+
visionLoginTimeoutLabel: "登录超时(秒)",
|
|
156
|
+
visionLoginTimeoutHint: "deepseek_vision_login 阻塞等待上限;DEEPSEEK_LOGIN_TIMEOUT 环境变量优先",
|
|
157
|
+
visionTimeoutLabel: "识图超时(毫秒)",
|
|
158
|
+
visionTimeoutHint: "deepseek_vision / deepseek_ocr 单次调用超时",
|
|
159
|
+
visionStatusTimeoutLabel: "状态超时(毫秒)",
|
|
160
|
+
visionStatusTimeoutHint: "deepseek_vision_status / deepseek_vision_logout 超时",
|
|
161
|
+
sharedBinaryPathLabel: "二进制路径",
|
|
162
|
+
sharedBinaryPathHint: "visionary-server 路径,工具与桥接(deterministic 模式)共用;空 = DEEPSEEK_VISIONARY_BIN → PATH",
|
|
163
|
+
enabledLabel: "桥接启用",
|
|
164
|
+
enabledHint: "关闭后恢复宿主原行为(文本模型粘贴图片仍被拒绝)",
|
|
165
|
+
pastedDirLabel: "落盘目录",
|
|
166
|
+
pastedDirHint: "图片落盘目录,强制 0700 / 文件 0600,支持 ~",
|
|
167
|
+
retainHoursLabel: "保留小时数",
|
|
168
|
+
retainHoursHint: "落盘副本保留小时数,<= 0 表示不清理",
|
|
169
|
+
scopeLabel: "桥接范围",
|
|
170
|
+
scopeHint: "text-only:仅文本模型 | also-vl:VL 模型同样经桥接",
|
|
171
|
+
modeLabel: "桥接模式",
|
|
172
|
+
modeHint: "agentic:改写为引导文本 | deterministic:直接调用分析",
|
|
173
|
+
promptTemplateLabel: "引导模板",
|
|
174
|
+
promptTemplateHint: "必须包含 {path} 占位符",
|
|
175
|
+
cleanPastedLabel: "清理已落盘副本",
|
|
176
|
+
cleanPastedHint: "一次性操作:删除落盘目录中的图片副本(附件库不受影响),清理数量写入 DSH 日志",
|
|
177
|
+
cleanPastedAction: "立即清理",
|
|
178
|
+
cleanPastedTriggered: "已触发清理",
|
|
179
|
+
expand: "展开设置",
|
|
180
|
+
collapse: "收起设置",
|
|
181
|
+
save: "保存",
|
|
182
|
+
saving: "保存中…",
|
|
183
|
+
discard: "放弃修改",
|
|
184
|
+
unsaved: "未保存",
|
|
185
|
+
saveFailed: "保存失败,已保留供修改。",
|
|
186
|
+
saveConflict: "保存被拒绝:配置已在别处修改,已重新读取。",
|
|
187
|
+
overridden: "已覆盖",
|
|
188
|
+
reset: "恢复默认",
|
|
189
|
+
readOnly: "本部署设置为只读。",
|
|
190
|
+
loading: "加载中…",
|
|
191
|
+
unavailable: "设置服务不可用。",
|
|
192
|
+
invalidNumber: "请输入数字;留空表示使用默认值。",
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
var LOCALE_EN = {
|
|
196
|
+
visionTitle: "Visionary · Vision Tools",
|
|
197
|
+
visionDescription: "deepseek_vision / deepseek_ocr and 3 more native tools: upload pipeline, timeouts and binary path. Changes apply immediately",
|
|
198
|
+
bridgeTitle: "Visionary · Image Bridge",
|
|
199
|
+
bridgeDescription: "Transparent image admission and rewrite for text-only models; VL models keep their native handling",
|
|
200
|
+
advanced: "Advanced",
|
|
201
|
+
advancedHint: "Rarely used: timeouts, storage and template",
|
|
202
|
+
visionModelTypeLabel: "Upload pipeline",
|
|
203
|
+
visionModelTypeHint: "vision (default): full multimodal understanding | ocr: deepseek_vision routes through text-extraction, same as deepseek_ocr. Applies immediately",
|
|
204
|
+
visionLoginTimeoutLabel: "Login timeout (s)",
|
|
205
|
+
visionLoginTimeoutHint: "deepseek_vision_login block cap; DEEPSEEK_LOGIN_TIMEOUT env wins",
|
|
206
|
+
visionTimeoutLabel: "Vision timeout (ms)",
|
|
207
|
+
visionTimeoutHint: "per deepseek_vision / deepseek_ocr call timeout",
|
|
208
|
+
visionStatusTimeoutLabel: "Status timeout (ms)",
|
|
209
|
+
visionStatusTimeoutHint: "deepseek_vision_status / deepseek_vision_logout timeout",
|
|
210
|
+
sharedBinaryPathLabel: "Binary path",
|
|
211
|
+
sharedBinaryPathHint: "visionary-server binary, shared by the tools and the bridge (deterministic mode); empty = DEEPSEEK_VISIONARY_BIN → PATH",
|
|
212
|
+
enabledLabel: "Bridge enabled",
|
|
213
|
+
enabledHint: "Off restores host behavior (text-only models reject images again)",
|
|
214
|
+
pastedDirLabel: "Paste directory",
|
|
215
|
+
pastedDirHint: "Image save dir (0700 dir / 0600 files), supports ~",
|
|
216
|
+
retainHoursLabel: "Retention (hours)",
|
|
217
|
+
retainHoursHint: "Pasted file retention; <= 0 disables cleanup",
|
|
218
|
+
scopeLabel: "Bridge scope",
|
|
219
|
+
scopeHint: "text-only: text models only | also-vl: VL models bridged too",
|
|
220
|
+
modeLabel: "Bridge mode",
|
|
221
|
+
modeHint: "agentic: rewrite to guide | deterministic: analyze directly",
|
|
222
|
+
promptTemplateLabel: "Prompt template",
|
|
223
|
+
promptTemplateHint: "Must contain the {path} placeholder",
|
|
224
|
+
cleanPastedLabel: "Clean pasted copies",
|
|
225
|
+
cleanPastedHint: "One-shot action: delete the pasted image copies (the attachment store is untouched); the removed count lands in the DSH log",
|
|
226
|
+
cleanPastedAction: "Clean now",
|
|
227
|
+
cleanPastedTriggered: "Cleanup triggered",
|
|
228
|
+
expand: "Expand settings",
|
|
229
|
+
collapse: "Collapse settings",
|
|
230
|
+
save: "Save",
|
|
231
|
+
saving: "Saving…",
|
|
232
|
+
discard: "Discard",
|
|
233
|
+
unsaved: "Unsaved",
|
|
234
|
+
saveFailed: "Save failed; values left for you to correct.",
|
|
235
|
+
saveConflict: "Save refused: the config changed elsewhere and was re-read.",
|
|
236
|
+
overridden: "Overridden",
|
|
237
|
+
reset: "Restore default",
|
|
238
|
+
readOnly: "This deployment stores settings read-only.",
|
|
239
|
+
loading: "Loading…",
|
|
240
|
+
unavailable: "Settings service unavailable.",
|
|
241
|
+
invalidNumber: "Enter a number, or leave blank to use the default.",
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// ── editor: staged edits over one card's namespaces ──
|
|
245
|
+
// Reads come from the bound scopes; writes are queued per namespace through
|
|
246
|
+
// `mutate(ops, revision)`, so every write carries the revision this editor
|
|
247
|
+
// read as its fence. A rejected write never throws: the scope reloads the
|
|
248
|
+
// host state, and the verification pass below turns the mismatch into the
|
|
249
|
+
// conflict notice instead of silently dropping the edit.
|
|
250
|
+
|
|
251
|
+
function makeEditor(opts) {
|
|
252
|
+
var fields = opts.fields;
|
|
253
|
+
var fieldsById = {};
|
|
254
|
+
fields.forEach(function (f) { fieldsById[f.id] = f; });
|
|
255
|
+
|
|
256
|
+
var listeners = new Set();
|
|
257
|
+
var staged = {};
|
|
258
|
+
var saving = false;
|
|
259
|
+
var failed = false;
|
|
260
|
+
var conflict = false;
|
|
261
|
+
var triggered = false;
|
|
262
|
+
var cache = null;
|
|
263
|
+
|
|
264
|
+
var scopeOf = function (ns) { return opts.scopes[ns]; };
|
|
265
|
+
var primary = function () { return scopeOf(opts.primaryNs).getSnapshot(); };
|
|
266
|
+
|
|
267
|
+
var displayValue = function (f) {
|
|
268
|
+
for (var i = 0; i < f.targets.length; i++) {
|
|
269
|
+
var target = f.targets[i];
|
|
270
|
+
var snapshot = scopeOf(target.ns).getSnapshot();
|
|
271
|
+
var raw = (snapshot.value || {})[target.key];
|
|
272
|
+
if (raw !== undefined && raw !== null && raw !== "") return String(raw);
|
|
273
|
+
}
|
|
274
|
+
return "";
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
var overridden = function (f) {
|
|
278
|
+
for (var i = 0; i < f.targets.length; i++) {
|
|
279
|
+
var target = f.targets[i];
|
|
280
|
+
var snapshot = scopeOf(target.ns).getSnapshot();
|
|
281
|
+
var user = snapshot.user;
|
|
282
|
+
if (user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, target.key)) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
var rebuild = function () {
|
|
290
|
+
var snap = primary();
|
|
291
|
+
var ready = snap.status === "ready";
|
|
292
|
+
var out = {
|
|
293
|
+
status: snap.status,
|
|
294
|
+
writable: snap.writable,
|
|
295
|
+
mode: snap.mode,
|
|
296
|
+
ready: ready,
|
|
297
|
+
dirty: Object.keys(staged).length > 0,
|
|
298
|
+
invalid: false,
|
|
299
|
+
saving: saving,
|
|
300
|
+
failed: failed,
|
|
301
|
+
conflict: conflict,
|
|
302
|
+
triggered: triggered,
|
|
303
|
+
fields: {},
|
|
304
|
+
};
|
|
305
|
+
fields.forEach(function (f) {
|
|
306
|
+
var stagedEntry = Object.prototype.hasOwnProperty.call(staged, f.id) ? staged[f.id] : undefined;
|
|
307
|
+
var text;
|
|
308
|
+
var fieldOverridden;
|
|
309
|
+
var invalid = false;
|
|
310
|
+
if (f.kind === "trigger") {
|
|
311
|
+
text = "";
|
|
312
|
+
fieldOverridden = false;
|
|
313
|
+
} else if (stagedEntry !== undefined) {
|
|
314
|
+
text = stagedEntry.text;
|
|
315
|
+
fieldOverridden = stagedEntry.text !== displayValue(f);
|
|
316
|
+
if (f.kind === "number" && stagedEntry.text.trim() !== "") {
|
|
317
|
+
invalid = !Number.isFinite(Number(stagedEntry.text.trim()));
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
text = displayValue(f);
|
|
321
|
+
fieldOverridden = overridden(f);
|
|
322
|
+
}
|
|
323
|
+
if (invalid) out.invalid = true;
|
|
324
|
+
out.fields[f.id] = { text: text, overridden: fieldOverridden, invalid: invalid };
|
|
325
|
+
});
|
|
326
|
+
cache = out;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
var publish = function () { rebuild(); listeners.forEach(function (l) { l(); }); };
|
|
330
|
+
|
|
331
|
+
var store = {
|
|
332
|
+
getSnapshot: function () { return cache; },
|
|
333
|
+
subscribe: function (listener) {
|
|
334
|
+
listeners.add(listener);
|
|
335
|
+
return function () { listeners.delete(listener); };
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
opts.ctx.effect(function () {
|
|
340
|
+
var cleanups = Object.keys(opts.scopes).map(function (ns) {
|
|
341
|
+
return opts.scopes[ns].subscribe(function () { publish(); });
|
|
342
|
+
});
|
|
343
|
+
return function () { cleanups.forEach(function (cancel) { cancel(); }); };
|
|
344
|
+
}, "visionary-settings: " + opts.primaryNs + " scope subscription");
|
|
345
|
+
|
|
346
|
+
rebuild();
|
|
347
|
+
|
|
348
|
+
var coerce = function (f, text) {
|
|
349
|
+
if (f.kind === "boolean") return text === "true";
|
|
350
|
+
if (f.kind === "number") return Number(text.trim());
|
|
351
|
+
return text;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
var edit = function (fieldId, text) {
|
|
355
|
+
var f = fieldsById[fieldId];
|
|
356
|
+
if (!f) return;
|
|
357
|
+
if (text === displayValue(f)) delete staged[fieldId];
|
|
358
|
+
else staged[fieldId] = { text: text };
|
|
359
|
+
failed = false;
|
|
360
|
+
conflict = false;
|
|
361
|
+
publish();
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
var discard = function () {
|
|
365
|
+
staged = {};
|
|
366
|
+
failed = false;
|
|
367
|
+
conflict = false;
|
|
368
|
+
triggered = false;
|
|
369
|
+
publish();
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Group staged edits by namespace: one `mutate` call per namespace, all
|
|
373
|
+
// ops sharing that namespace's read revision as the fence.
|
|
374
|
+
var groupOps = function () {
|
|
375
|
+
var byNs = {};
|
|
376
|
+
for (var fieldId in staged) {
|
|
377
|
+
if (!Object.prototype.hasOwnProperty.call(staged, fieldId)) continue;
|
|
378
|
+
var f = fieldsById[fieldId];
|
|
379
|
+
var text = staged[fieldId].text;
|
|
380
|
+
f.targets.forEach(function (target) {
|
|
381
|
+
if (!byNs[target.ns]) byNs[target.ns] = { ops: [], expect: [] };
|
|
382
|
+
if (text.trim() === "") {
|
|
383
|
+
byNs[target.ns].ops.push({ op: "unset", path: [target.key] });
|
|
384
|
+
byNs[target.ns].expect.push({ key: target.key, value: undefined });
|
|
385
|
+
} else {
|
|
386
|
+
var value = coerce(f, text);
|
|
387
|
+
byNs[target.ns].ops.push({ op: "set", path: [target.key], value: value });
|
|
388
|
+
byNs[target.ns].expect.push({ key: target.key, value: value });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return byNs;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// Did the host accept what we wrote? The scope reloads on rejection, so a
|
|
396
|
+
// stale fence shows up here as "the value is not what we wrote".
|
|
397
|
+
var verify = function (byNs) {
|
|
398
|
+
for (var ns in byNs) {
|
|
399
|
+
if (!Object.prototype.hasOwnProperty.call(byNs, ns)) continue;
|
|
400
|
+
var snapshot = scopeOf(ns).getSnapshot();
|
|
401
|
+
var value = snapshot.value || {};
|
|
402
|
+
var user = snapshot.user || {};
|
|
403
|
+
var expects = byNs[ns].expect;
|
|
404
|
+
for (var i = 0; i < expects.length; i++) {
|
|
405
|
+
var expected = expects[i];
|
|
406
|
+
if (expected.value === undefined) {
|
|
407
|
+
if (Object.prototype.hasOwnProperty.call(user, expected.key)) return false;
|
|
408
|
+
} else if (value[expected.key] !== expected.value) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return true;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
var save = function () {
|
|
417
|
+
if (!cache.dirty || cache.invalid || saving) return Promise.resolve();
|
|
418
|
+
var byNs = groupOps();
|
|
419
|
+
saving = true;
|
|
420
|
+
failed = false;
|
|
421
|
+
conflict = false;
|
|
422
|
+
publish();
|
|
423
|
+
var writes = Object.keys(byNs).map(function (ns) {
|
|
424
|
+
return scopeOf(ns).mutate(byNs[ns].ops, scopeOf(ns).getSnapshot().revision);
|
|
425
|
+
});
|
|
426
|
+
return Promise.all(writes)
|
|
427
|
+
.then(function () {
|
|
428
|
+
if (!verify(byNs)) conflict = true;
|
|
429
|
+
else staged = {};
|
|
430
|
+
})
|
|
431
|
+
.catch(function () { failed = true; })
|
|
432
|
+
.then(function () {
|
|
433
|
+
saving = false;
|
|
434
|
+
publish();
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
var reset = function (fieldId) {
|
|
439
|
+
var f = fieldsById[fieldId];
|
|
440
|
+
if (!f || !cache.writable) return Promise.resolve();
|
|
441
|
+
delete staged[fieldId];
|
|
442
|
+
var writes = f.targets.map(function (target) {
|
|
443
|
+
return scopeOf(target.ns).unset(target.key);
|
|
444
|
+
});
|
|
445
|
+
return Promise.all(writes)
|
|
446
|
+
.catch(function () { failed = true; })
|
|
447
|
+
.then(function () { publish(); });
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// One-shot trigger (cleanPasted): write `true`; the host clears the files
|
|
451
|
+
// and resets the persisted value, which the scope mirror then reports.
|
|
452
|
+
var trigger = function (fieldId) {
|
|
453
|
+
var f = fieldsById[fieldId];
|
|
454
|
+
if (!f || !cache.writable) return Promise.resolve();
|
|
455
|
+
var ns = f.targets[0].ns;
|
|
456
|
+
triggered = true;
|
|
457
|
+
publish();
|
|
458
|
+
return scopeOf(ns)
|
|
459
|
+
.mutate([{ op: "set", path: [f.key], value: true }], scopeOf(ns).getSnapshot().revision)
|
|
460
|
+
.catch(function () { failed = true; })
|
|
461
|
+
.then(function () { publish(); });
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
store: store,
|
|
466
|
+
edit: edit,
|
|
467
|
+
reset: reset,
|
|
468
|
+
save: save,
|
|
469
|
+
discard: discard,
|
|
470
|
+
trigger: trigger,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── React plumbing ──
|
|
475
|
+
|
|
476
|
+
function useStore(store) {
|
|
477
|
+
var pair = React.useState(function () { return store.getSnapshot(); });
|
|
478
|
+
var snapshot = pair[0];
|
|
479
|
+
var setSnapshot = pair[1];
|
|
480
|
+
React.useEffect(function () {
|
|
481
|
+
setSnapshot(store.getSnapshot());
|
|
482
|
+
return store.subscribe(function () { setSnapshot(store.getSnapshot()); });
|
|
483
|
+
}, [store]);
|
|
484
|
+
return snapshot;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── card chrome ──
|
|
488
|
+
//
|
|
489
|
+
// A `settings.plugin.item` row is normally drawn by the host's own card
|
|
490
|
+
// module (`dsh-client-ui-settings-plugins`), whose CSS-module class names are
|
|
491
|
+
// hashed per build. Those hashed names are unusable outside that package, so
|
|
492
|
+
// the rules are mirrored here under our own `vlb-` prefix with the identical
|
|
493
|
+
// declarations and `--dsw-alias-*` tokens; a token rename costs appearance,
|
|
494
|
+
// while reaching for the hashed names would break outright.
|
|
495
|
+
|
|
496
|
+
var CARD_CSS = [
|
|
497
|
+
".vlb-card{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);border-radius:16px;list-style:none;transition:border-color .16s,background .16s}",
|
|
498
|
+
".vlb-card:hover{border-color:var(--dsw-alias-label-dimmed)}",
|
|
499
|
+
".vlb-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
|
|
500
|
+
".vlb-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}",
|
|
501
|
+
".vlb-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
|
|
502
|
+
".vlb-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
|
|
503
|
+
".vlb-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
|
|
504
|
+
".vlb-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
|
|
505
|
+
".vlb-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
|
|
506
|
+
".vlb-chevronOpen{transform:rotate(180deg)}",
|
|
507
|
+
".vlb-tag{display:inline-flex;align-items:center;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}",
|
|
508
|
+
".vlb-pending{flex:none}",
|
|
509
|
+
".vlb-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}",
|
|
510
|
+
".vlb-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}",
|
|
511
|
+
".vlb-field+.vlb-field{border-top:.5px solid var(--dsw-alias-border-l2)}",
|
|
512
|
+
".vlb-fieldHead{align-items:center;gap:8px;display:flex}",
|
|
513
|
+
".vlb-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}",
|
|
514
|
+
".vlb-badges{align-items:center;gap:8px;display:inline-flex}",
|
|
515
|
+
".vlb-reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}",
|
|
516
|
+
".vlb-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}",
|
|
517
|
+
".vlb-reset:disabled{cursor:default;opacity:.4}",
|
|
518
|
+
".vlb-toggleRow{justify-content:space-between;align-items:flex-start;gap:16px;display:flex}",
|
|
519
|
+
".vlb-toggleLabel{flex:1;min-width:0;gap:6px;flex-direction:column;display:flex}",
|
|
520
|
+
".vlb-input{box-sizing:border-box;width:100%;border:.5px solid var(--dsw-alias-border-l4);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;line-height:1.5}",
|
|
521
|
+
".vlb-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}",
|
|
522
|
+
".vlb-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}",
|
|
523
|
+
".vlb-inputInvalid{border-color:var(--dsw-alias-label-error)}",
|
|
524
|
+
".vlb-textarea{box-sizing:border-box;width:100%;min-height:72px;resize:vertical;border:.5px solid var(--dsw-alias-border-l4);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.5}",
|
|
525
|
+
".vlb-textarea:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}",
|
|
526
|
+
".vlb-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}",
|
|
527
|
+
".vlb-invalid{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}",
|
|
528
|
+
".vlb-disclosure{appearance:none;width:100%;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;padding:12px 0;font-size:13px;line-height:1.5;display:flex}",
|
|
529
|
+
".vlb-disclosure:hover{color:var(--dsw-alias-label-primary)}",
|
|
530
|
+
".vlb-footer{border-top:.5px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}",
|
|
531
|
+
".vlb-failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}",
|
|
532
|
+
".vlb-discard,.vlb-save,.vlb-trigger{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}",
|
|
533
|
+
".vlb-discard,.vlb-trigger{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}",
|
|
534
|
+
".vlb-discard:hover:not(:disabled),.vlb-trigger:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}",
|
|
535
|
+
".vlb-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}",
|
|
536
|
+
".vlb-discard:disabled,.vlb-save:disabled,.vlb-trigger:disabled{opacity:.4;cursor:default}",
|
|
537
|
+
".vlb-discard:focus-visible,.vlb-save:focus-visible,.vlb-trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}",
|
|
538
|
+
".vlb-readOnly,.vlb-loading{margin:12px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}",
|
|
539
|
+
".vlb-unavailable{margin:12px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}",
|
|
540
|
+
].join("");
|
|
541
|
+
|
|
542
|
+
// The host's IconChevronDownOutline14, inlined: one less module edge.
|
|
543
|
+
var CHEVRON_PATH = "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";
|
|
544
|
+
|
|
545
|
+
function chevron(className) {
|
|
546
|
+
return React.createElement("svg", {
|
|
547
|
+
className: className,
|
|
548
|
+
width: 14,
|
|
549
|
+
height: 14,
|
|
550
|
+
viewBox: "0 0 14 14",
|
|
551
|
+
fill: "none",
|
|
552
|
+
"aria-hidden": "true",
|
|
553
|
+
}, React.createElement("path", { d: CHEVRON_PATH, fill: "currentColor" }));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function makeCardComponent(editor, fields, t, titleKey, descriptionKey, advancedHintKey) {
|
|
557
|
+
return function VisionaryCard() {
|
|
558
|
+
var snapshot = useStore(editor.store);
|
|
559
|
+
var openState = React.useState(false);
|
|
560
|
+
var open = openState[0];
|
|
561
|
+
var setOpen = openState[1];
|
|
562
|
+
var disclosure = React.useState(false);
|
|
563
|
+
var advancedOpen = disclosure[0];
|
|
564
|
+
var setAdvancedOpen = disclosure[1];
|
|
565
|
+
var triggeredFlag = React.useState(false);
|
|
566
|
+
var justTriggered = triggeredFlag[0];
|
|
567
|
+
var setJustTriggered = triggeredFlag[1];
|
|
568
|
+
var saveStarted = React.useRef(false);
|
|
569
|
+
|
|
570
|
+
var disabled = !snapshot.writable || snapshot.saving;
|
|
571
|
+
var title = t(titleKey);
|
|
572
|
+
|
|
573
|
+
// Native cards collapse once a save lands; mirror that so a finished edit
|
|
574
|
+
// leaves the list in its compact state.
|
|
575
|
+
React.useEffect(function () {
|
|
576
|
+
if (snapshot.saving) { saveStarted.current = true; return; }
|
|
577
|
+
if (!saveStarted.current) return;
|
|
578
|
+
saveStarted.current = false;
|
|
579
|
+
if (!snapshot.dirty && !snapshot.failed && !snapshot.conflict) setOpen(false);
|
|
580
|
+
}, [snapshot.dirty, snapshot.failed, snapshot.conflict, snapshot.saving]);
|
|
581
|
+
|
|
582
|
+
var badges = function (f, state) {
|
|
583
|
+
if (!state.overridden) return null;
|
|
584
|
+
return React.createElement("span", { key: "badges", className: "vlb-badges" }, [
|
|
585
|
+
React.createElement("span", { key: "tag", className: "vlb-tag" }, t("overridden")),
|
|
586
|
+
React.createElement("button", {
|
|
587
|
+
key: "reset",
|
|
588
|
+
type: "button",
|
|
589
|
+
className: "vlb-reset",
|
|
590
|
+
disabled: disabled,
|
|
591
|
+
onClick: function () { editor.reset(f.id); },
|
|
592
|
+
}, t("reset")),
|
|
593
|
+
]);
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
var renderField = function (f) {
|
|
597
|
+
var state = snapshot.fields[f.id] || { text: "", overridden: false, invalid: false };
|
|
598
|
+
var controlId = "visionary-field-" + f.id.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
599
|
+
var onEdit = function (text) { editor.edit(f.id, text); };
|
|
600
|
+
|
|
601
|
+
if (f.kind === "boolean") {
|
|
602
|
+
return React.createElement("div", { key: f.id, className: "vlb-field" }, [
|
|
603
|
+
React.createElement("div", { key: "row", className: "vlb-toggleRow" }, [
|
|
604
|
+
React.createElement("div", { key: "text", className: "vlb-toggleLabel" }, [
|
|
605
|
+
React.createElement("span", { key: "label", className: "vlb-label" }, t(f.labelKey)),
|
|
606
|
+
badges(f, state),
|
|
607
|
+
]),
|
|
608
|
+
React.createElement(SwitchControl, {
|
|
609
|
+
key: "switch",
|
|
610
|
+
checked: state.text === "true",
|
|
611
|
+
label: t(f.labelKey),
|
|
612
|
+
disabled: disabled,
|
|
613
|
+
onChange: function (next) {
|
|
614
|
+
onEdit(String(typeof next === "boolean" ? next : state.text !== "true"));
|
|
615
|
+
},
|
|
616
|
+
}),
|
|
617
|
+
]),
|
|
618
|
+
React.createElement("p", { key: "hint", className: "vlb-hint" }, t(f.hintKey)),
|
|
619
|
+
]);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
var control;
|
|
623
|
+
if (f.kind === "trigger") {
|
|
624
|
+
control = React.createElement("button", {
|
|
625
|
+
type: "button",
|
|
626
|
+
id: controlId,
|
|
627
|
+
className: "vlb-trigger",
|
|
628
|
+
disabled: disabled,
|
|
629
|
+
onClick: function () {
|
|
630
|
+
setJustTriggered(true);
|
|
631
|
+
editor.trigger(f.id);
|
|
632
|
+
},
|
|
633
|
+
}, justTriggered ? t("cleanPastedTriggered") : t(f.actionKey));
|
|
634
|
+
} else if (f.kind === "select") {
|
|
635
|
+
control = React.createElement("select", {
|
|
636
|
+
id: controlId,
|
|
637
|
+
className: "vlb-input",
|
|
638
|
+
value: state.text,
|
|
639
|
+
disabled: disabled,
|
|
640
|
+
onChange: function (e) { onEdit(e.target.value); },
|
|
641
|
+
}, f.options.map(function (o) { return React.createElement("option", { key: o, value: o }, o); }));
|
|
642
|
+
} else if (f.kind === "number") {
|
|
643
|
+
control = React.createElement("input", {
|
|
644
|
+
id: controlId,
|
|
645
|
+
className: state.invalid ? "vlb-input vlb-inputInvalid" : "vlb-input",
|
|
646
|
+
type: "text",
|
|
647
|
+
inputMode: "numeric",
|
|
648
|
+
"aria-invalid": state.invalid ? "true" : "false",
|
|
649
|
+
value: state.text,
|
|
650
|
+
disabled: disabled,
|
|
651
|
+
onChange: function (e) { onEdit(e.target.value); },
|
|
652
|
+
});
|
|
653
|
+
} else if (f.kind === "textarea") {
|
|
654
|
+
control = React.createElement("textarea", {
|
|
655
|
+
id: controlId,
|
|
656
|
+
className: "vlb-textarea",
|
|
657
|
+
value: state.text,
|
|
658
|
+
disabled: disabled,
|
|
659
|
+
placeholder: t(f.placeholderKey || f.hintKey),
|
|
660
|
+
onChange: function (e) { onEdit(e.target.value); },
|
|
661
|
+
});
|
|
662
|
+
} else {
|
|
663
|
+
control = React.createElement("input", {
|
|
664
|
+
id: controlId,
|
|
665
|
+
className: "vlb-input",
|
|
666
|
+
type: "text",
|
|
667
|
+
value: state.text,
|
|
668
|
+
disabled: disabled,
|
|
669
|
+
onChange: function (e) { onEdit(e.target.value); },
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return React.createElement("div", { key: f.id, className: "vlb-field" }, [
|
|
674
|
+
React.createElement("div", { key: "head", className: "vlb-fieldHead" }, [
|
|
675
|
+
React.createElement("label", { key: "label", className: "vlb-label", htmlFor: controlId }, t(f.labelKey)),
|
|
676
|
+
badges(f, state),
|
|
677
|
+
]),
|
|
678
|
+
control,
|
|
679
|
+
React.createElement("p", { key: "hint", className: state.invalid ? "vlb-invalid" : "vlb-hint" },
|
|
680
|
+
state.invalid ? t("invalidNumber") : t(f.hintKey)),
|
|
681
|
+
]);
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
var body = [];
|
|
685
|
+
if (snapshot.status === "loading") {
|
|
686
|
+
body.push(React.createElement("p", { key: "loading", className: "vlb-loading" }, t("loading")));
|
|
687
|
+
} else if (snapshot.status === "unavailable") {
|
|
688
|
+
body.push(React.createElement("p", { key: "unavailable", className: "vlb-unavailable" }, t("unavailable")));
|
|
689
|
+
} else {
|
|
690
|
+
var advancedFields = fieldsIn(fields, "advanced");
|
|
691
|
+
if (!snapshot.writable) {
|
|
692
|
+
body.push(React.createElement("p", { key: "ro", className: "vlb-readOnly" }, t("readOnly")));
|
|
693
|
+
}
|
|
694
|
+
body.push(React.createElement("div", { key: "main" }, fieldsIn(fields, "main").map(renderField)));
|
|
695
|
+
if (advancedFields.length > 0) {
|
|
696
|
+
body.push(React.createElement("button", {
|
|
697
|
+
key: "disclosure",
|
|
698
|
+
type: "button",
|
|
699
|
+
className: "vlb-disclosure",
|
|
700
|
+
"aria-expanded": advancedOpen ? "true" : "false",
|
|
701
|
+
onClick: function () { setAdvancedOpen(!advancedOpen); },
|
|
702
|
+
}, [
|
|
703
|
+
chevron(advancedOpen ? "vlb-chevron vlb-chevronOpen" : "vlb-chevron"),
|
|
704
|
+
React.createElement("span", { key: "text" }, t("advanced") + " · " + t(advancedHintKey)),
|
|
705
|
+
]));
|
|
706
|
+
if (advancedOpen) {
|
|
707
|
+
body.push(React.createElement("div", { key: "advanced" }, advancedFields.map(renderField)));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
body.push(React.createElement("div", { key: "footer", className: "vlb-footer" }, [
|
|
711
|
+
(snapshot.failed || snapshot.conflict)
|
|
712
|
+
? React.createElement("p", { key: "err", className: "vlb-failed" },
|
|
713
|
+
snapshot.conflict ? t("saveConflict") : t("saveFailed"))
|
|
714
|
+
: null,
|
|
715
|
+
React.createElement("button", {
|
|
716
|
+
key: "discard",
|
|
717
|
+
type: "button",
|
|
718
|
+
className: "vlb-discard",
|
|
719
|
+
disabled: !snapshot.dirty || snapshot.saving,
|
|
720
|
+
onClick: function () { editor.discard(); setJustTriggered(false); },
|
|
721
|
+
}, t("discard")),
|
|
722
|
+
React.createElement("button", {
|
|
723
|
+
key: "save",
|
|
724
|
+
type: "button",
|
|
725
|
+
className: "vlb-save",
|
|
726
|
+
disabled: snapshot.status !== "ready" || !snapshot.dirty || snapshot.invalid || snapshot.saving,
|
|
727
|
+
onClick: function () { editor.save(); },
|
|
728
|
+
}, snapshot.saving ? t("saving") : t("save")),
|
|
729
|
+
]));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return React.createElement("li", { className: open ? "vlb-card vlb-cardOpen" : "vlb-card" }, [
|
|
733
|
+
React.createElement("button", {
|
|
734
|
+
key: "header",
|
|
735
|
+
type: "button",
|
|
736
|
+
className: "vlb-header",
|
|
737
|
+
"aria-expanded": open ? "true" : "false",
|
|
738
|
+
"aria-label": t(open ? "collapse" : "expand") + ": " + title,
|
|
739
|
+
onClick: function () { setOpen(!open); },
|
|
740
|
+
}, [
|
|
741
|
+
React.createElement("span", { key: "text", className: "vlb-headText" }, [
|
|
742
|
+
React.createElement("span", { key: "name", className: "vlb-name" }, title),
|
|
743
|
+
React.createElement("span", { key: "desc", className: "vlb-description" }, t(descriptionKey)),
|
|
744
|
+
]),
|
|
745
|
+
snapshot.dirty
|
|
746
|
+
? React.createElement("span", { key: "pending", className: "vlb-tag vlb-pending" }, t("unsaved"))
|
|
747
|
+
: null,
|
|
748
|
+
chevron(open ? "vlb-chevron vlb-chevronOpen" : "vlb-chevron"),
|
|
749
|
+
]),
|
|
750
|
+
open ? React.createElement("div", { key: "body", className: "vlb-body" }, body) : null,
|
|
751
|
+
]);
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Split one card's fields into the always-visible region and the collapsed
|
|
756
|
+
// "advanced" region.
|
|
757
|
+
function fieldsIn(fields, area) {
|
|
758
|
+
return fields.filter(function (f) { return f.area === area; });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ── apply ──
|
|
762
|
+
|
|
763
|
+
var inject = ["slots", "locale", "settingsScope"];
|
|
764
|
+
|
|
765
|
+
function apply(ctx) {
|
|
766
|
+
ctx.effect(function () {
|
|
767
|
+
return ctx.locale.register(NS, { zh: LOCALE_ZH, en: LOCALE_EN });
|
|
768
|
+
}, "locale: " + NS);
|
|
769
|
+
// Card chrome (CARD_CSS): one tagged <style> for both cards, removed with
|
|
770
|
+
// the fiber so a disabled or updated row leaves no styles behind.
|
|
771
|
+
ctx.effect(function () {
|
|
772
|
+
if (typeof document === "undefined") return undefined;
|
|
773
|
+
var style = document.createElement("style");
|
|
774
|
+
style.setAttribute("data-plugin", "@xlight-oss/visionary-dsh");
|
|
775
|
+
style.textContent = CARD_CSS;
|
|
776
|
+
document.head.appendChild(style);
|
|
777
|
+
return function () { style.remove(); };
|
|
778
|
+
}, "visionary-settings: card chrome");
|
|
779
|
+
var t = ctx.locale.bind(NS);
|
|
780
|
+
|
|
781
|
+
var scopes = {};
|
|
782
|
+
scopes[NS_VISION] = ctx.settingsScope.bind({ namespace: NS_VISION });
|
|
783
|
+
scopes[NS_BRIDGE] = ctx.settingsScope.bind({ namespace: NS_BRIDGE });
|
|
784
|
+
|
|
785
|
+
var visionEditor = makeEditor({ ctx: ctx, scopes: scopes, primaryNs: NS_VISION, fields: VISION_FIELDS });
|
|
786
|
+
var bridgeEditor = makeEditor({ ctx: ctx, scopes: scopes, primaryNs: NS_BRIDGE, fields: BRIDGE_FIELDS });
|
|
787
|
+
|
|
788
|
+
var VisionCard = makeCardComponent(visionEditor, VISION_FIELDS, t, "visionTitle", "visionDescription", "advancedHint");
|
|
789
|
+
var BridgeCard = makeCardComponent(bridgeEditor, BRIDGE_FIELDS, t, "bridgeTitle", "bridgeDescription", "advancedHint");
|
|
790
|
+
|
|
791
|
+
ctx.slots.inject("settings.plugin.item", function () {
|
|
792
|
+
return ctx.slots.register({
|
|
793
|
+
name: "settings.plugin.item",
|
|
794
|
+
key: NS_VISION,
|
|
795
|
+
order: 20,
|
|
796
|
+
locale: NS,
|
|
797
|
+
}, VisionCard);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
ctx.slots.inject("settings.plugin.item", function () {
|
|
801
|
+
return ctx.slots.register({
|
|
802
|
+
name: "settings.plugin.item",
|
|
803
|
+
key: NS_BRIDGE,
|
|
804
|
+
order: 21,
|
|
805
|
+
locale: NS,
|
|
806
|
+
}, BridgeCard);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
exports.inject = inject;
|
|
811
|
+
exports.apply = apply;
|
|
812
|
+
return module.exports;
|
|
813
|
+
}
|
|
814
|
+
});
|