@1outres/dsh-plugin-background 0.1.1 → 0.3.0
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 -3
- package/cordis.patch.yml +6 -0
- package/lib/client.js +134 -22
- package/package.json +19 -7
package/README.md
CHANGED
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Paint an image behind the whole DeepSeek Harness Web UI. The interface stays
|
|
4
4
|
usable: panels become translucent, a configurable veil keeps text readable, and
|
|
5
|
-
the image can be blurred.
|
|
5
|
+
the image can be blurred. The Settings dialog stays opaque so its controls remain
|
|
6
|
+
readable over any image.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Install just this plugin:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
dsh plugin add @1outres/dsh-plugin-background
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or install it with every plugin in the repository through the
|
|
15
|
+
[`@1outres/dsh-plugins`](../dsh-plugins) bundle. Enable one or the other: both
|
|
16
|
+
insert the same `background` row.
|
|
8
17
|
|
|
9
18
|
## Settings
|
|
10
19
|
|
|
@@ -44,7 +53,10 @@ a component. Instead it:
|
|
|
44
53
|
2. reads the active theme's alias colors from the stylesheet and re-registers
|
|
45
54
|
them through `ctx.theme.overrideTokens()` as `color-mix(... transparent)`
|
|
46
55
|
surfaces, so panels, cards, popovers and the sidebar let the image through;
|
|
47
|
-
3.
|
|
56
|
+
3. restores the opaque alias colors inside the body-portaled Settings dialog
|
|
57
|
+
(`[data-shortcut-modal="settings"]`), so opening Settings never shows the
|
|
58
|
+
image through the panel;
|
|
59
|
+
4. registers a `settings.section` page that reads and writes the Host settings
|
|
48
60
|
entry (`background`) through `ctx.configForms`.
|
|
49
61
|
|
|
50
62
|
Theme switches re-read the palette, so the effect follows light and dark mode.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Installs this plugin on its own (dsh plugin add @1outres/dsh-plugin-background).
|
|
2
|
+
# The @1outres/dsh-plugins bundle inserts the same "background" row, so select
|
|
3
|
+
# one bundle or the other, never both.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: background
|
|
6
|
+
name: "@1outres/dsh-plugin-background"
|
package/lib/client.js
CHANGED
|
@@ -104,6 +104,7 @@ window.__ModuleLoader__.load({
|
|
|
104
104
|
title: "Background",
|
|
105
105
|
description: "Paint an image behind the whole interface. Point at an absolute path on the machine running DSH, or at an https URL.",
|
|
106
106
|
enable: "Enable background",
|
|
107
|
+
enableHint: "Turns the wallpaper off without clearing the image or the readability controls.",
|
|
107
108
|
image: "Image path or URL",
|
|
108
109
|
imageHint: "Absolute local path, for example /home/you/wallpaper.jpg, or an https:// URL. Local images are served through the authenticated file route, so they never leave the Host.",
|
|
109
110
|
invalidImage: "Enter an absolute path or an http(s) URL.",
|
|
@@ -126,6 +127,7 @@ window.__ModuleLoader__.load({
|
|
|
126
127
|
};
|
|
127
128
|
|
|
128
129
|
// src/client/SettingsPage.tsx
|
|
130
|
+
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
129
131
|
var import_react = require("react");
|
|
130
132
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
131
133
|
var FIELDS = ["enabled", "image", "panelOpacity", "dim", "blur"];
|
|
@@ -166,6 +168,21 @@ window.__ModuleLoader__.load({
|
|
|
166
168
|
fontSize: 12,
|
|
167
169
|
lineHeight: 1.5
|
|
168
170
|
};
|
|
171
|
+
var switchHintStyle = {
|
|
172
|
+
...hintStyle,
|
|
173
|
+
marginTop: 4
|
|
174
|
+
};
|
|
175
|
+
var switchRowStyle = {
|
|
176
|
+
display: "flex",
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
justifyContent: "space-between",
|
|
179
|
+
gap: 24,
|
|
180
|
+
marginTop: 22
|
|
181
|
+
};
|
|
182
|
+
var switchTitleStyle = {
|
|
183
|
+
fontSize: 13,
|
|
184
|
+
fontWeight: 600
|
|
185
|
+
};
|
|
169
186
|
var errorStyle = {
|
|
170
187
|
margin: 0,
|
|
171
188
|
color: "var(--dsw-alias-state-error-primary)",
|
|
@@ -214,6 +231,51 @@ window.__ModuleLoader__.load({
|
|
|
214
231
|
fontSize: 13,
|
|
215
232
|
lineHeight: 1.6
|
|
216
233
|
};
|
|
234
|
+
var SLIDER_COMMIT_DELAY = 300;
|
|
235
|
+
function useSliderDraft(committed, commit) {
|
|
236
|
+
const [value, setValue] = (0, import_react.useState)(committed);
|
|
237
|
+
const latest = (0, import_react.useRef)(committed);
|
|
238
|
+
const dirty = (0, import_react.useRef)(false);
|
|
239
|
+
const inFlight = (0, import_react.useRef)(0);
|
|
240
|
+
const timer = (0, import_react.useRef)(void 0);
|
|
241
|
+
(0, import_react.useEffect)(() => {
|
|
242
|
+
if (dirty.current || inFlight.current > 0) return;
|
|
243
|
+
latest.current = committed;
|
|
244
|
+
setValue(committed);
|
|
245
|
+
}, [committed]);
|
|
246
|
+
(0, import_react.useEffect)(
|
|
247
|
+
() => () => {
|
|
248
|
+
window.clearTimeout(timer.current);
|
|
249
|
+
},
|
|
250
|
+
[]
|
|
251
|
+
);
|
|
252
|
+
const send = () => {
|
|
253
|
+
window.clearTimeout(timer.current);
|
|
254
|
+
timer.current = void 0;
|
|
255
|
+
if (!dirty.current) return;
|
|
256
|
+
dirty.current = false;
|
|
257
|
+
const sent = latest.current;
|
|
258
|
+
inFlight.current += 1;
|
|
259
|
+
void commit(sent).then((accepted) => {
|
|
260
|
+
inFlight.current -= 1;
|
|
261
|
+
if (accepted || dirty.current || inFlight.current > 0) return;
|
|
262
|
+
latest.current = committed;
|
|
263
|
+
setValue(committed);
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
const change = (next) => {
|
|
267
|
+
if (next === latest.current) return;
|
|
268
|
+
latest.current = next;
|
|
269
|
+
dirty.current = true;
|
|
270
|
+
setValue(next);
|
|
271
|
+
window.clearTimeout(timer.current);
|
|
272
|
+
timer.current = window.setTimeout(send, SLIDER_COMMIT_DELAY);
|
|
273
|
+
};
|
|
274
|
+
const flush = () => {
|
|
275
|
+
send();
|
|
276
|
+
};
|
|
277
|
+
return { value, change, flush };
|
|
278
|
+
}
|
|
217
279
|
function SettingsPage({ form, t }) {
|
|
218
280
|
const subscribe = (0, import_react.useCallback)((listener) => form.subscribe(listener), [form]);
|
|
219
281
|
const getSnapshot = (0, import_react.useCallback)(() => form.getSnapshot(), [form]);
|
|
@@ -231,12 +293,22 @@ window.__ModuleLoader__.load({
|
|
|
231
293
|
const draftSource = resolveImageSource(draft, document.baseURI);
|
|
232
294
|
const draftInvalid = draft.trim().length > 0 && draftSource === void 0;
|
|
233
295
|
const previewFailed = previewFailure !== null && previewFailure.src === source && previewFailure.failed;
|
|
234
|
-
const write = (field, value) =>
|
|
235
|
-
|
|
236
|
-
|
|
296
|
+
const write = (field, value) => form.set(field, value).then(
|
|
297
|
+
(accepted) => {
|
|
298
|
+
setWriteError(!accepted);
|
|
299
|
+
return accepted;
|
|
300
|
+
},
|
|
301
|
+
() => {
|
|
302
|
+
setWriteError(true);
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
const opacitySlider = useSliderDraft(config.panelOpacity, (value) => write("panelOpacity", value));
|
|
307
|
+
const dimSlider = useSliderDraft(config.dim, (value) => write("dim", value));
|
|
308
|
+
const blurSlider = useSliderDraft(config.blur, (value) => write("blur", value));
|
|
237
309
|
const commitImage = () => {
|
|
238
310
|
const next = draft.trim();
|
|
239
|
-
if (next !== config.image) write("image", next);
|
|
311
|
+
if (next !== config.image) void write("image", next);
|
|
240
312
|
};
|
|
241
313
|
const onImageChange = (event) => {
|
|
242
314
|
setDraft(event.target.value);
|
|
@@ -254,23 +326,30 @@ window.__ModuleLoader__.load({
|
|
|
254
326
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: mutedStyle, children: t("unavailable") });
|
|
255
327
|
}
|
|
256
328
|
const disabled = snapshot.writable === false;
|
|
257
|
-
const dim = dimColor(
|
|
258
|
-
const alpha = clamp(
|
|
329
|
+
const dim = dimColor(dimSlider.value);
|
|
330
|
+
const alpha = clamp(opacitySlider.value, 0, 100) / 100;
|
|
331
|
+
const blur = clamp(blurSlider.value, 0, 40);
|
|
259
332
|
const dark = document.body.hasAttribute("data-ds-dark-theme");
|
|
260
333
|
const surfaceColor = dark ? `rgba(21, 21, 23, ${alpha})` : `rgba(255, 255, 255, ${alpha})`;
|
|
261
334
|
const surfaceText = dark ? "#e9ecf2" : "#0f1115";
|
|
262
335
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: sectionStyle, children: [
|
|
263
336
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: headingStyle, children: t("title") }),
|
|
264
337
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: introStyle, children: t("description") }),
|
|
265
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("
|
|
266
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
338
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: switchRowStyle, children: [
|
|
339
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { minWidth: 0 }, children: [
|
|
340
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: switchTitleStyle, children: t("enable") }),
|
|
341
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: switchHintStyle, children: t("enableHint") })
|
|
342
|
+
] }),
|
|
267
343
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
268
|
-
|
|
344
|
+
import_dsh_client_ui_primitives.Switch,
|
|
269
345
|
{
|
|
270
|
-
type: "checkbox",
|
|
271
346
|
checked: config.enabled,
|
|
272
347
|
disabled,
|
|
273
|
-
|
|
348
|
+
label: t("enable"),
|
|
349
|
+
title: disabled ? t("readOnly") : void 0,
|
|
350
|
+
onChange: (next) => {
|
|
351
|
+
void write("enabled", next);
|
|
352
|
+
}
|
|
274
353
|
}
|
|
275
354
|
)
|
|
276
355
|
] }),
|
|
@@ -307,7 +386,7 @@ window.__ModuleLoader__.load({
|
|
|
307
386
|
backgroundImage: imageCssValue(source),
|
|
308
387
|
backgroundPosition: "center",
|
|
309
388
|
backgroundSize: "cover",
|
|
310
|
-
filter: `blur(${
|
|
389
|
+
filter: `blur(${blur}px)`,
|
|
311
390
|
transform: "scale(1.06)"
|
|
312
391
|
}
|
|
313
392
|
}
|
|
@@ -364,7 +443,7 @@ window.__ModuleLoader__.load({
|
|
|
364
443
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: labelRowStyle, htmlFor: "dsh-background-panel-opacity", children: [
|
|
365
444
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("panelOpacity") }),
|
|
366
445
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: mutedStyle, children: [
|
|
367
|
-
Math.round(
|
|
446
|
+
Math.round(opacitySlider.value),
|
|
368
447
|
"%"
|
|
369
448
|
] })
|
|
370
449
|
] }),
|
|
@@ -376,10 +455,14 @@ window.__ModuleLoader__.load({
|
|
|
376
455
|
min: 0,
|
|
377
456
|
max: 100,
|
|
378
457
|
step: 1,
|
|
379
|
-
value:
|
|
458
|
+
value: opacitySlider.value,
|
|
380
459
|
disabled,
|
|
381
460
|
style: sliderStyle,
|
|
382
|
-
onChange: (event) =>
|
|
461
|
+
onChange: (event) => opacitySlider.change(Number(event.target.value)),
|
|
462
|
+
onPointerUp: opacitySlider.flush,
|
|
463
|
+
onPointerCancel: opacitySlider.flush,
|
|
464
|
+
onKeyUp: opacitySlider.flush,
|
|
465
|
+
onBlur: opacitySlider.flush
|
|
383
466
|
}
|
|
384
467
|
),
|
|
385
468
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: hintStyle, children: t("panelOpacityHint") })
|
|
@@ -388,7 +471,7 @@ window.__ModuleLoader__.load({
|
|
|
388
471
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: labelRowStyle, htmlFor: "dsh-background-dim", children: [
|
|
389
472
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("dim") }),
|
|
390
473
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: mutedStyle, children: [
|
|
391
|
-
Math.round(
|
|
474
|
+
Math.round(dimSlider.value),
|
|
392
475
|
"%"
|
|
393
476
|
] })
|
|
394
477
|
] }),
|
|
@@ -400,10 +483,14 @@ window.__ModuleLoader__.load({
|
|
|
400
483
|
min: 0,
|
|
401
484
|
max: 100,
|
|
402
485
|
step: 1,
|
|
403
|
-
value:
|
|
486
|
+
value: dimSlider.value,
|
|
404
487
|
disabled,
|
|
405
488
|
style: sliderStyle,
|
|
406
|
-
onChange: (event) =>
|
|
489
|
+
onChange: (event) => dimSlider.change(Number(event.target.value)),
|
|
490
|
+
onPointerUp: dimSlider.flush,
|
|
491
|
+
onPointerCancel: dimSlider.flush,
|
|
492
|
+
onKeyUp: dimSlider.flush,
|
|
493
|
+
onBlur: dimSlider.flush
|
|
407
494
|
}
|
|
408
495
|
),
|
|
409
496
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: hintStyle, children: t("dimHint") })
|
|
@@ -412,7 +499,7 @@ window.__ModuleLoader__.load({
|
|
|
412
499
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: labelRowStyle, htmlFor: "dsh-background-blur", children: [
|
|
413
500
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("blur") }),
|
|
414
501
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: mutedStyle, children: [
|
|
415
|
-
Math.round(
|
|
502
|
+
Math.round(blurSlider.value),
|
|
416
503
|
" px"
|
|
417
504
|
] })
|
|
418
505
|
] }),
|
|
@@ -424,10 +511,14 @@ window.__ModuleLoader__.load({
|
|
|
424
511
|
min: 0,
|
|
425
512
|
max: 40,
|
|
426
513
|
step: 1,
|
|
427
|
-
value:
|
|
514
|
+
value: blurSlider.value,
|
|
428
515
|
disabled,
|
|
429
516
|
style: sliderStyle,
|
|
430
|
-
onChange: (event) =>
|
|
517
|
+
onChange: (event) => blurSlider.change(Number(event.target.value)),
|
|
518
|
+
onPointerUp: blurSlider.flush,
|
|
519
|
+
onPointerCancel: blurSlider.flush,
|
|
520
|
+
onKeyUp: blurSlider.flush,
|
|
521
|
+
onBlur: blurSlider.flush
|
|
431
522
|
}
|
|
432
523
|
),
|
|
433
524
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: hintStyle, children: t("blurHint") })
|
|
@@ -469,6 +560,18 @@ window.__ModuleLoader__.load({
|
|
|
469
560
|
z-index: 2;
|
|
470
561
|
}
|
|
471
562
|
`;
|
|
563
|
+
var SETTINGS_DIALOG = '[data-shortcut-modal="settings"]';
|
|
564
|
+
function settingsSurfaceCss(solid) {
|
|
565
|
+
const declarations = Object.entries(solid).filter(([, value]) => isSafeCssValue(value)).map(([name, value]) => ` ${name}: ${value};`);
|
|
566
|
+
if (declarations.length === 0) return "";
|
|
567
|
+
return `html[data-dsh-background="on"] ${SETTINGS_DIALOG} {
|
|
568
|
+
${declarations.join("\n")}
|
|
569
|
+
}
|
|
570
|
+
`;
|
|
571
|
+
}
|
|
572
|
+
function isSafeCssValue(value) {
|
|
573
|
+
return value.length > 0 && !/[{;}@]/.test(value) && !value.includes("\n");
|
|
574
|
+
}
|
|
472
575
|
|
|
473
576
|
// src/client/index.tsx
|
|
474
577
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
@@ -479,13 +582,18 @@ window.__ModuleLoader__.load({
|
|
|
479
582
|
function apply(ctx) {
|
|
480
583
|
const t = ctx.locale.bind(CONFIG_NAMESPACE);
|
|
481
584
|
ctx.effect(() => ctx.locale.register(CONFIG_NAMESPACE, { en }), "background: dictionaries");
|
|
585
|
+
let styleTag;
|
|
482
586
|
ctx.effect(() => {
|
|
483
587
|
const tag = document.createElement("style");
|
|
484
588
|
tag.dataset.plugin = PLUGIN_ID;
|
|
485
589
|
tag.dataset.pluginCss = `${PLUGIN_ID}/background.css`;
|
|
486
590
|
tag.textContent = BACKGROUND_CSS;
|
|
487
591
|
document.head.append(tag);
|
|
488
|
-
|
|
592
|
+
styleTag = tag;
|
|
593
|
+
return () => {
|
|
594
|
+
tag.remove();
|
|
595
|
+
styleTag = void 0;
|
|
596
|
+
};
|
|
489
597
|
}, "background: stylesheet");
|
|
490
598
|
const form = ctx.configForms.get(CONFIG_NAMESPACE);
|
|
491
599
|
let tokenDispose;
|
|
@@ -501,14 +609,17 @@ window.__ModuleLoader__.load({
|
|
|
501
609
|
tokenDispose?.();
|
|
502
610
|
tokenDispose = void 0;
|
|
503
611
|
const tokens = {};
|
|
612
|
+
const solid = {};
|
|
504
613
|
for (const name of TOKEN_NAMES) {
|
|
505
614
|
const base = readBaseColor(name);
|
|
506
615
|
if (base === void 0) continue;
|
|
507
616
|
const percent = name === OVERLAY_TOKEN ? Math.min(100, config.panelOpacity + 20) : config.panelOpacity;
|
|
508
617
|
const value = alphaColor(base, percent);
|
|
509
618
|
tokens[name] = { light: value, dark: value };
|
|
619
|
+
solid[name] = base;
|
|
510
620
|
}
|
|
511
621
|
if (Object.keys(tokens).length > 0) tokenDispose = ctx.theme.overrideTokens(PLUGIN_ID, tokens);
|
|
622
|
+
if (styleTag !== void 0) styleTag.textContent = BACKGROUND_CSS + settingsSurfaceCss(solid);
|
|
512
623
|
};
|
|
513
624
|
const deactivate = () => {
|
|
514
625
|
activeConfig = void 0;
|
|
@@ -519,6 +630,7 @@ window.__ModuleLoader__.load({
|
|
|
519
630
|
root.style.removeProperty("--dsh-background-image");
|
|
520
631
|
root.style.removeProperty("--dsh-background-blur");
|
|
521
632
|
root.style.removeProperty("--dsh-background-dim");
|
|
633
|
+
if (styleTag !== void 0) styleTag.textContent = BACKGROUND_CSS;
|
|
522
634
|
};
|
|
523
635
|
const applyConfig = (config) => {
|
|
524
636
|
if (applying) return;
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1outres/dsh-plugin-background",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Set a background image behind the DeepSeek Harness Web UI, with panel opacity, dim and blur controls.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/1outres/my-dsh-plugins.git",
|
|
8
|
+
"directory": "packages/dsh-plugin-background"
|
|
9
|
+
},
|
|
5
10
|
"type": "module",
|
|
6
11
|
"main": "lib/index.js",
|
|
7
12
|
"exports": {
|
|
@@ -10,11 +15,9 @@
|
|
|
10
15
|
"./package.json": "./package.json"
|
|
11
16
|
},
|
|
12
17
|
"files": [
|
|
13
|
-
"lib"
|
|
18
|
+
"lib",
|
|
19
|
+
"cordis.patch.yml"
|
|
14
20
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"prepublishOnly": "node ../../scripts/build.mjs"
|
|
17
|
-
},
|
|
18
21
|
"keywords": [
|
|
19
22
|
"dsh",
|
|
20
23
|
"dsh-plugin",
|
|
@@ -38,6 +41,11 @@
|
|
|
38
41
|
"react": "^18.2.0"
|
|
39
42
|
},
|
|
40
43
|
"dsh": {
|
|
44
|
+
"bundle": {
|
|
45
|
+
"patch": [
|
|
46
|
+
"./cordis.patch.yml"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
41
49
|
"client": {
|
|
42
50
|
"platform": "web",
|
|
43
51
|
"inject": [
|
|
@@ -46,5 +54,9 @@
|
|
|
46
54
|
"@deepseek-ai/dsh-client-ui-theme"
|
|
47
55
|
]
|
|
48
56
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {}
|
|
62
|
+
}
|