@linxin666/dsh-client-ui-skin-center 0.1.1 → 0.1.2

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 CHANGED
@@ -29,9 +29,10 @@
29
29
 
30
30
  ```sh
31
31
  # 装全部皮肤(推荐)
32
- dsh plugin --profile web add link:$(pwd)/packages/dsh-skins
32
+ dsh plugin --profile web add @linxin666/dsh-skins
33
33
  # 或单独装皮肤中心
34
- dsh plugin --profile web add link:$(pwd)/packages/skins/skin-center
34
+ dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center
35
+ # 从仓库安装(开发调试):dsh plugin --profile web add link:$(pwd)/packages/skins/skin-center
35
36
  ```
36
37
 
37
38
  `$(pwd)` 指克隆全家桶仓库后的目录。
package/lib/client.js CHANGED
@@ -425,7 +425,7 @@ window.__ModuleLoader__.load({
425
425
  };
426
426
  //#endregion
427
427
  //#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/skins/skin-center/src/client/skin-center.module.css.mjs
428
- const css = "body[data-dsh-skin-center] .K3rRTW_pluginCard{border:1px solid var(--dsw-alias-border-l1,#e2e8f0);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:8px;list-style:none;overflow:hidden}body[data-dsh-skin-center] .K3rRTW_cardHeader{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;padding:11px 14px;transition:background .12s;display:flex}body[data-dsh-skin-center] .K3rRTW_cardHeader:hover{background:var(--dsw-alias-bg-layer-1,#f1f5f9)}body[data-dsh-skin-center] .K3rRTW_cardHeader:active{background:var(--dsw-alias-bg-layer-3,#e6ecf4)}body[data-dsh-skin-center] .K3rRTW_cardHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_headText{flex-direction:column;flex:1;gap:3px;min-width:0;display:flex}body[data-dsh-skin-center] .K3rRTW_pluginName{color:var(--dsw-alias-label-primary,#172a45);align-items:baseline;gap:8px;font-size:13.5px;font-weight:600;display:flex}body[data-dsh-skin-center] .K3rRTW_cardDescription{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.4}body[data-dsh-skin-center] .K3rRTW_chevron,body[data-dsh-skin-center] .K3rRTW_chevronOpen{color:var(--dsw-alias-label-secondary,#6b7280);flex:none;margin-left:10px;font-size:12px;transition:transform .12s}body[data-dsh-skin-center] .K3rRTW_chevronOpen{transform:rotate(180deg)}body[data-dsh-skin-center] .K3rRTW_cardBody{border-top:1px solid var(--dsw-alias-border-l1,#e2e8f0);flex-direction:column;gap:12px;padding:12px 14px 14px;display:flex}body[data-dsh-skin-center] .K3rRTW_head{flex-direction:column;gap:6px;display:flex}body[data-dsh-skin-center] .K3rRTW_titleBadge{color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;font-weight:500}body[data-dsh-skin-center] .K3rRTW_intro{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12.5px;line-height:1.55}body[data-dsh-skin-center] .K3rRTW_themeRow{align-items:center;gap:8px;margin-top:2px;display:flex}body[data-dsh-skin-center] .K3rRTW_themeLabel{color:var(--dsw-alias-label-secondary,#6b7280);margin-right:2px;font-size:12px}body[data-dsh-skin-center] .K3rRTW_themeButton{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:6px;padding:5px 10px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .K3rRTW_themeButton:hover{border-color:var(--dsw-alias-border-l4,#94a3b8)}body[data-dsh-skin-center] .K3rRTW_themeButton:active{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_themeButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_themeButtonActive{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_list{flex-direction:column;gap:10px;display:flex}body[data-dsh-skin-center] .K3rRTW_card{border:1px solid var(--dsw-alias-border-l1,#e2e8f0);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:10px;flex-direction:column;gap:8px;padding:12px 14px;display:flex}body[data-dsh-skin-center] .K3rRTW_cardHead{align-items:center;gap:10px;min-width:0;display:flex}body[data-dsh-skin-center] .K3rRTW_swatch{width:14px;height:14px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);border-radius:50%;flex:none}body[data-dsh-skin-center] .K3rRTW_cardName{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:13.5px;font-weight:600;overflow:hidden}body[data-dsh-skin-center] .K3rRTW_cardTagline{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.45}body[data-dsh-skin-center] .K3rRTW_badge{letter-spacing:.02em;border-radius:999px;flex:none;min-width:0;margin-left:auto;padding:2px 8px;font-size:11px;font-weight:600}body[data-dsh-skin-center] .K3rRTW_badgeActive{color:var(--dsw-alias-state-success-primary,#0f6b3a);background:var(--dsw-alias-state-success-tertiary,#dcf3e5)}body[data-dsh-skin-center] .K3rRTW_badgeTrying{color:var(--dsw-alias-brand-primary,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e2edfc)}body[data-dsh-skin-center] .K3rRTW_actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}body[data-dsh-skin-center] .K3rRTW_button{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:7px;padding:6px 12px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .K3rRTW_button:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2b7cd9);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_button:active:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_buttonPrimary{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-fill,#2b7cd9);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .K3rRTW_buttonPrimary:hover:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .K3rRTW_buttonPrimary:active:not(:disabled),body[data-dsh-skin-center] .K3rRTW_buttonPrimary:focus-visible:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_buttonGhost{background:0 0;border-color:#0000}body[data-dsh-skin-center] .K3rRTW_button:disabled{opacity:.55;cursor:default}body[data-dsh-skin-center] .K3rRTW_error{color:var(--dsw-alias-state-error-primary,#b42318);font-size:12px}@media (prefers-reduced-motion:reduce){body[data-dsh-skin-center] .K3rRTW_cardHeader,body[data-dsh-skin-center] .K3rRTW_themeButton,body[data-dsh-skin-center] .K3rRTW_button,body[data-dsh-skin-center] .K3rRTW_chevron,body[data-dsh-skin-center] .K3rRTW_chevronOpen{transition:none}}";
428
+ const css = "body[data-dsh-skin-center] .K3rRTW_pluginCard{border:1px solid var(--dsw-alias-border-l1,#e2e8f0);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:8px;list-style:none;overflow:hidden}body[data-dsh-skin-center] .K3rRTW_cardHeader{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;padding:11px 14px;transition:background .12s;display:flex}body[data-dsh-skin-center] .K3rRTW_cardHeader:hover{background:var(--dsw-alias-bg-layer-1,#f1f5f9)}body[data-dsh-skin-center] .K3rRTW_cardHeader:active{background:var(--dsw-alias-bg-layer-3,#e6ecf4)}body[data-dsh-skin-center] .K3rRTW_cardHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_headText{flex-direction:column;flex:1;gap:3px;min-width:0;display:flex}body[data-dsh-skin-center] .K3rRTW_pluginName{color:var(--dsw-alias-label-primary,#172a45);align-items:baseline;gap:8px;font-size:13.5px;font-weight:600;display:flex}body[data-dsh-skin-center] .K3rRTW_cardDescription{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.4}body[data-dsh-skin-center] .K3rRTW_chevron,body[data-dsh-skin-center] .K3rRTW_chevronOpen{color:var(--dsw-alias-label-secondary,#6b7280);flex:none;margin-left:10px;font-size:12px;transition:transform .12s}body[data-dsh-skin-center] .K3rRTW_chevronOpen{transform:rotate(180deg)}body[data-dsh-skin-center] .K3rRTW_cardBody{border-top:1px solid var(--dsw-alias-border-l1,#e2e8f0);flex-direction:column;gap:12px;padding:12px 14px 14px;display:flex}body[data-dsh-skin-center] .K3rRTW_head{flex-direction:column;gap:6px;display:flex}body[data-dsh-skin-center] .K3rRTW_titleBadge{color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;font-weight:500}body[data-dsh-skin-center] .K3rRTW_intro{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12.5px;line-height:1.55}body[data-dsh-skin-center] .K3rRTW_themeRow{align-items:center;gap:8px;margin-top:2px;display:flex}body[data-dsh-skin-center] .K3rRTW_themeLabel{color:var(--dsw-alias-label-secondary,#6b7280);margin-right:2px;font-size:12px}body[data-dsh-skin-center] .K3rRTW_themeButton{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:6px;padding:5px 10px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .K3rRTW_themeButton:hover{border-color:var(--dsw-alias-border-l4,#94a3b8)}body[data-dsh-skin-center] .K3rRTW_themeButton:active{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_themeButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_themeButtonActive{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_list{flex-direction:column;gap:10px;display:flex}body[data-dsh-skin-center] .K3rRTW_card{border:1px solid var(--dsw-alias-border-l1,#e2e8f0);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:10px;flex-direction:column;gap:8px;padding:12px 14px;display:flex}body[data-dsh-skin-center] .K3rRTW_cardHead{align-items:center;gap:10px;min-width:0;display:flex}body[data-dsh-skin-center] .K3rRTW_swatch{width:14px;height:14px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);border-radius:50%;flex:none}body[data-dsh-skin-center] .K3rRTW_cardName{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:13.5px;font-weight:600;overflow:hidden}body[data-dsh-skin-center] .K3rRTW_cardTagline{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.45}body[data-dsh-skin-center] .K3rRTW_badge{letter-spacing:.02em;border-radius:999px;flex:none;min-width:0;margin-left:auto;padding:2px 8px;font-size:11px;font-weight:600}body[data-dsh-skin-center] .K3rRTW_badgeActive{color:var(--dsw-alias-state-success-primary,#0f6b3a);background:var(--dsw-alias-state-success-tertiary,#dcf3e5)}body[data-dsh-skin-center] .K3rRTW_badgeTrying{color:var(--dsw-alias-brand-primary,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e2edfc)}body[data-dsh-skin-center] .K3rRTW_actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}body[data-dsh-skin-center] .K3rRTW_button{border:1px solid var(--dsw-alias-border-l3,#cbd5e1);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#172a45);cursor:pointer;border-radius:7px;padding:6px 12px;font-size:12px;line-height:1;transition:background .12s,border-color .12s,color .12s}body[data-dsh-skin-center] .K3rRTW_button:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary,#2b7cd9);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_button:active:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-dimmed,#e8f1fc);color:var(--dsw-alias-brand-primary,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_buttonPrimary{border-color:var(--dsw-alias-brand-primary,#2b7cd9);background:var(--dsw-alias-button-primary-fill,#2b7cd9);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .K3rRTW_buttonPrimary:hover:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8);color:var(--dsw-alias-label-primary-foreground,#fff)}body[data-dsh-skin-center] .K3rRTW_buttonPrimary:active:not(:disabled),body[data-dsh-skin-center] .K3rRTW_buttonPrimary:focus-visible:not(:disabled){border-color:var(--dsw-alias-button-primary-hover,#1e63b8);background:var(--dsw-alias-button-primary-hover,#1e63b8)}body[data-dsh-skin-center] .K3rRTW_buttonGhost{background:0 0;border-color:#0000}body[data-dsh-skin-center] .K3rRTW_button:disabled{opacity:.55;cursor:default}body[data-dsh-skin-center] .K3rRTW_error{color:var(--dsw-alias-state-error-primary,#b42318);font-size:12px}body[data-dsh-skin-center] .K3rRTW_backgroundRow{flex-direction:column;gap:6px;padding:8px 0;display:flex}body[data-dsh-skin-center] .K3rRTW_backgroundHead{align-items:center;gap:8px;display:flex}body[data-dsh-skin-center] .K3rRTW_backgroundLabel{color:var(--dsw-alias-label-primary,#172a45);font-size:12.5px;font-weight:600}body[data-dsh-skin-center] .K3rRTW_backgroundValue{font-variant-numeric:tabular-nums;color:var(--dsw-alias-brand-primary,#2b7cd9);flex:none;margin-left:auto;font-size:12px}body[data-dsh-skin-center] .K3rRTW_backgroundRange{background:var(--dsw-alias-bg-layer-3,#e2e8f0);-webkit-appearance:none;appearance:none;cursor:pointer;border-radius:999px;width:100%;height:4px;margin:0}body[data-dsh-skin-center] .K3rRTW_backgroundRange::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-brand-primary,#2b7cd9);width:14px;height:14px;box-shadow:0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);cursor:pointer;border-radius:50%}body[data-dsh-skin-center] .K3rRTW_backgroundRange::-moz-range-thumb{border:2px solid var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-brand-primary,#2b7cd9);width:12px;height:12px;box-shadow:0 0 0 1px var(--dsw-alias-border-l4,#0f172a1f);cursor:pointer;border-radius:50%}body[data-dsh-skin-center] .K3rRTW_backgroundRange:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2b7cd9);outline-offset:2px}body[data-dsh-skin-center] .K3rRTW_backgroundHint{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1.5}body[data-dsh-skin-center] .K3rRTW_backgroundHintMuted{color:var(--dsw-alias-label-tertiary,#9aa4b5);font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){body[data-dsh-skin-center] .K3rRTW_cardHeader,body[data-dsh-skin-center] .K3rRTW_themeButton,body[data-dsh-skin-center] .K3rRTW_button,body[data-dsh-skin-center] .K3rRTW_chevron,body[data-dsh-skin-center] .K3rRTW_chevronOpen{transition:none}}";
429
429
  const tagId = "@linxin666/dsh-client-ui-skin-center/skin-center.module.css";
430
430
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
431
431
  const tag = document.createElement("style");
@@ -436,6 +436,13 @@ window.__ModuleLoader__.load({
436
436
  }
437
437
  var skin_center_module_css_default = {
438
438
  "actions": "K3rRTW_actions",
439
+ "backgroundHead": "K3rRTW_backgroundHead",
440
+ "backgroundHint": "K3rRTW_backgroundHint",
441
+ "backgroundHintMuted": "K3rRTW_backgroundHintMuted",
442
+ "backgroundLabel": "K3rRTW_backgroundLabel",
443
+ "backgroundRange": "K3rRTW_backgroundRange",
444
+ "backgroundRow": "K3rRTW_backgroundRow",
445
+ "backgroundValue": "K3rRTW_backgroundValue",
439
446
  "badge": "K3rRTW_badge",
440
447
  "badgeActive": "K3rRTW_badgeActive",
441
448
  "badgeTrying": "K3rRTW_badgeTrying",
@@ -479,6 +486,8 @@ window.__ModuleLoader__.load({
479
486
  */
480
487
  /** The apply target of the official stock-look card. */
481
488
  const OFFICIAL = "official";
489
+ /** Skin ids that read the background-scrim variable and paint a backdrop. */
490
+ const BACKDROP_SKIN_IDS = /* @__PURE__ */ new Set(["blue-fantasy", "whale-song"]);
482
491
  /**
483
492
  * Render the skin-center card: a disclosure header naming the plugin, with
484
493
  * the skin list (official default + every installed skin; try-on / theme
@@ -486,9 +495,12 @@ window.__ModuleLoader__.load({
486
495
  * @param props - card props.
487
496
  * @returns the plugin card.
488
497
  */
489
- function SkinCenter({ t, controller, theme }) {
498
+ function SkinCenter({ t, controller, theme, background }) {
490
499
  const snapshot = (0, react.useSyncExternalStore)(theme.subscribe, theme.getTheme);
500
+ const opacity = (0, react.useSyncExternalStore)(background.subscribe, background.opacity);
491
501
  const activePackage = activeSkinEntry()?.package;
502
+ const activeId = activeSkinEntry()?.id;
503
+ const backdropActive = activeId !== void 0 && BACKDROP_SKIN_IDS.has(activeId);
492
504
  const [open, setOpen] = (0, react.useState)(false);
493
505
  const [tryingId, setTryingId] = (0, react.useState)(null);
494
506
  const [tryingOfficial, setTryingOfficial] = (0, react.useState)(false);
@@ -670,6 +682,40 @@ window.__ModuleLoader__.load({
670
682
  ]
671
683
  })]
672
684
  }),
685
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
686
+ className: skin_center_module_css_default.backgroundRow,
687
+ children: [
688
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
689
+ className: skin_center_module_css_default.backgroundHead,
690
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
691
+ className: skin_center_module_css_default.backgroundLabel,
692
+ children: t("backgroundOpacity")
693
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
694
+ className: skin_center_module_css_default.backgroundValue,
695
+ "aria-hidden": "true",
696
+ children: [opacity, "%"]
697
+ })]
698
+ }),
699
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
700
+ id: "skin-center-background-opacity",
701
+ className: skin_center_module_css_default.backgroundRange,
702
+ type: "range",
703
+ min: "0",
704
+ max: "100",
705
+ step: "5",
706
+ value: opacity,
707
+ "aria-valuetext": `${opacity}%`,
708
+ "aria-label": t("backgroundOpacity"),
709
+ onChange: (event) => {
710
+ background.set(Number(event.target.value));
711
+ }
712
+ }),
713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
714
+ className: backdropActive ? skin_center_module_css_default.backgroundHint : skin_center_module_css_default.backgroundHintMuted,
715
+ children: backdropActive ? t("backgroundHint") : t("backgroundHintInert")
716
+ })
717
+ ]
718
+ }),
673
719
  error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
674
720
  className: skin_center_module_css_default.error,
675
721
  children: error
@@ -765,6 +811,65 @@ window.__ModuleLoader__.load({
765
811
  });
766
812
  }
767
813
  //#endregion
814
+ //#region src/client/background.ts
815
+ /** The namespace string the Host registers (mirrors src/index.ts). */
816
+ const SKIN_BACKGROUND_NS = "skin-background";
817
+ /** Field of the background value inside the namespace section. */
818
+ const OPACITY_FIELD = "backgroundOpacity";
819
+ /** CSS custom property written to document.body and read by backdrop skins. */
820
+ const SCRIM_VAR = "--dsw-skin-scrim";
821
+ /**
822
+ * Own the skin-background scope: read the latest occlusion, apply it to the
823
+ * body CSS variable instantly, and persist changes through the settings scope.
824
+ */
825
+ var BackgroundController = class {
826
+ value = 0;
827
+ listeners = /* @__PURE__ */ new Set();
828
+ scope;
829
+ /**
830
+ * @param scope - the bound skin-background settings scope.
831
+ */
832
+ constructor(scope) {
833
+ this.scope = scope;
834
+ this.value = this.read();
835
+ this.apply();
836
+ scope.subscribe(() => {
837
+ this.value = this.read();
838
+ this.apply();
839
+ this.publish();
840
+ });
841
+ }
842
+ opacity() {
843
+ return this.value;
844
+ }
845
+ subscribe(listener) {
846
+ this.listeners.add(listener);
847
+ return () => {
848
+ this.listeners.delete(listener);
849
+ };
850
+ }
851
+ set(opacity) {
852
+ const clamped = Math.max(0, Math.min(100, Math.round(opacity)));
853
+ this.value = clamped;
854
+ this.apply();
855
+ this.publish();
856
+ this.scope.set(OPACITY_FIELD, clamped);
857
+ }
858
+ /** The effective section value, clamped 0-100, defaulting to 0. */
859
+ read() {
860
+ const raw = this.scope.getSnapshot().value?.backgroundOpacity;
861
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return 0;
862
+ return Math.max(0, Math.min(100, raw));
863
+ }
864
+ /** Write the current occlusion onto the body CSS variable (0..1 alpha). */
865
+ apply() {
866
+ document.body.style.setProperty(SCRIM_VAR, String(this.value / 100));
867
+ }
868
+ publish() {
869
+ for (const listener of this.listeners) listener();
870
+ }
871
+ };
872
+ //#endregion
768
873
  //#region src/client/locales.ts
769
874
  const en = {
770
875
  title: "Skin Center",
@@ -786,7 +891,10 @@ window.__ModuleLoader__.load({
786
891
  theme: "Theme preview",
787
892
  themeLight: "Light",
788
893
  themeDark: "Dark",
789
- tryOnError: "Try-on failed — see console"
894
+ tryOnError: "Try-on failed — see console",
895
+ backgroundOpacity: "Background occlusion",
896
+ backgroundHint: "Instantly veils the backdrop behind the panels — higher values obscure the art to help you focus.",
897
+ backgroundHintInert: "Only applies to skins that paint a backdrop (Blue Fantasy / Whale Song). Applies to the official default automatically once such a skin is active."
790
898
  };
791
899
  const zh = {
792
900
  title: "皮肤中心",
@@ -808,17 +916,23 @@ window.__ModuleLoader__.load({
808
916
  theme: "主题预览",
809
917
  themeLight: "亮色",
810
918
  themeDark: "暗色",
811
- tryOnError: "试穿失败,详见控制台"
919
+ tryOnError: "试穿失败,详见控制台",
920
+ backgroundOpacity: "背景遮挡",
921
+ backgroundHint: "即时为面板背后的背景加遮罩——数值越高越能弱化插画,帮你集中注意力。",
922
+ backgroundHintInert: "仅对带背景图插画的皮肤(蓝色幻想 / 鲸吟)生效;官方默认无背景图,该滑块对这些皮肤自动生效。"
812
923
  };
813
924
  //#endregion
814
925
  //#region src/client/index.ts
815
926
  /** Locale namespace owned by this plugin. */
816
927
  const NS = "skinCenter";
817
- /** Required services: slots + locale (plugin card) and theme (preview toggle). */
928
+ /** Required services: slots + locale (plugin card), theme (preview toggle), and settingsScope + its transport (background scrim). */
818
929
  const inject = [
819
930
  "slots",
820
931
  "locale",
821
- "theme"
932
+ "theme",
933
+ "settingsScope",
934
+ "connection",
935
+ "remote"
822
936
  ];
823
937
  /**
824
938
  * Register the skin-center dictionaries, the body scope attribute, and the
@@ -838,12 +952,18 @@ window.__ModuleLoader__.load({
838
952
  }, "ui-skin-center: body scope");
839
953
  const theme = ctx.get("theme");
840
954
  const controller = new TryOnController();
955
+ const background = new BackgroundController(ctx.settingsScope.bind({ namespace: SKIN_BACKGROUND_NS }));
841
956
  const injected = () => ({
842
957
  controller,
843
958
  theme: {
844
959
  getTheme: () => theme.getTheme(),
845
960
  subscribe: (listener) => ctx.on("theme/change", listener),
846
961
  setTheme: (id) => theme.setTheme(id)
962
+ },
963
+ background: {
964
+ opacity: () => background.opacity(),
965
+ subscribe: (listener) => background.subscribe(listener),
966
+ set: (opacity) => background.set(opacity)
847
967
  }
848
968
  });
849
969
  ctx.slots.inject("web-ui.plugin.item", () => ctx.slots.register({
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["css"],"sources":["../../../packages/skins/skin-center/src/client/generated/skins.ts","../../../packages/skins/skin-center/src/client/try-on.ts","../../../packages/skins/skin-center/src/client/SkinCenter.tsx","../../../packages/skins/skin-center/src/client/locales.ts","../../../packages/skins/skin-center/src/client/index.ts"],"sourcesContent":["/**\n * AUTO-GENERATED by scripts/skin-center-bundles — do not edit by hand.\n * Skin registry for the GUI skin center: gallery metadata only. Client\n * bundle text is NOT embedded here — try-on fetches each skin's\n * prebuilt lib/client.js on demand from the host route\n * /api/skin-center/bundle/<id> (a same-origin script executed by the\n * page's own module loader; no eval, and the GUI never parses the\n * embedded art base64 at startup).\n * Regenerate with `node scripts/skin-center-bundles`.\n */\n\n/** One registry entry: gallery metadata for the skin center card. */\nexport interface SkinCenterEntry {\n /** Skin id (packages/skins/<id>/). */\n id: string\n /** Display name (zh). */\n name: string\n /** Display name (en). */\n nameEn: string\n /** One-line tagline. */\n tagline: string\n /** Accent color for the swatch. */\n accent: string\n /** Author credit. */\n author?: string\n /** Longer description. */\n description?: string\n /** Tag list. */\n tags?: string[]\n /** Body attribute that scopes the skin styles. */\n bodyAttr: string\n /** Plugin package name (the boot-graph entry id when active). */\n package: string\n /** Gallery sort order (packages/skins/<name>/skin.json). */\n order?: number\n}\n\n/** Every skin, ordered by packages/skins/<name>/skin.json `order`. */\nexport const SKIN_CENTER_ENTRIES: readonly SkinCenterEntry[] = [\n {\"id\":\"qq98\",\"name\":\"QQ2008 怀旧版\",\"nameEn\":\"QQ2008 Retro\",\"author\":\"dsh-web-ui\",\"tagline\":\"水晶蓝桌面 · 玻璃深蓝标题栏 · 戴围巾企鹅\",\"description\":\"dsh web ui 家族收录的第一个皮肤:QQ2008 水晶蓝年代。深蓝渐变桌面、玻璃质感标题栏、浅蓝状态栏和圆角高光控件,配一只戴围巾的企鹅。\",\"tags\":[\"retro\",\"qq\",\"2008\",\"crystal-blue\",\"nostalgia\"],\"accent\":\"#2b7cd9\",\"bodyAttr\":\"data-dsh-retro\",\"package\":\"@linxin666/dsh-client-ui-skin-qq98\",\"order\":1},\n\n {\"id\":\"ths\",\"name\":\"同花顺风格\",\"nameEn\":\"Tonghuashun Trading\",\"author\":\"dsh-web-ui\",\"tagline\":\"品牌红标题栏 · 实时行情状态栏 · 灰蓝数据终端\",\"description\":\"同花顺风格炒股主题:品牌红标题栏带上证指数行情签,状态栏红涨绿跌,自选股风格的侧边栏和交易终端面板,写代码也像盯盘。\",\"tags\":[\"stock\",\"trading\",\"terminal\",\"red\"],\"accent\":\"#e60012\",\"bodyAttr\":\"data-dsh-ths\",\"package\":\"@linxin666/dsh-client-ui-skin-ths\",\"order\":2},\n\n {\"id\":\"xp\",\"name\":\"Windows XP (Luna)\",\"nameEn\":\"Windows XP Luna\",\"author\":\"dsh-web-ui\",\"tagline\":\"Luna 蓝窗口条 · 绿色开始按钮 · Bliss 蓝天桌面\",\"description\":\"Windows XP (Luna) 复古主题:蓝色渐变窗口条带窗口按钮、米色状态栏(大写/数字/滚动指示灯)、侧边栏任务栏上的绿色「开始」按钮、资源管理器风格树行和 Bliss 蓝天桌面,全局直角。\",\"tags\":[\"retro\",\"xp\",\"luna\",\"windows\",\"start-button\"],\"accent\":\"#316ac5\",\"bodyAttr\":\"data-dsh-xp\",\"package\":\"@linxin666/dsh-client-ui-skin-xp\",\"order\":3},\n\n {\"id\":\"blue-fantasy\",\"name\":\"蓝色幻想\",\"nameEn\":\"Blue Fantasy\",\"author\":\"powerdog996(DreamSkin 社区)· dsh-web-ui 适配\",\"tagline\":\"鲸鱼插画背景 · periwinkle 靛蓝调色板 · 半透明面板\",\"description\":\"DreamSkin「DeepSeek-鲸鱼娘」Codex 桌面主题的 dsh 适配:鲸鱼插画背景垫在半透明面板之下,遮罩随亮/暗主题实时切换,periwinkle 靛蓝色调重映射到全部 dsh token。\",\"tags\":[\"dreamskin\",\"whale\",\"indigo\",\"art\",\"translucent\"],\"accent\":\"#4a5fa8\",\"bodyAttr\":\"data-dsh-blue-fantasy\",\"package\":\"@linxin666/dsh-client-ui-skin-blue-fantasy\",\"order\":4},\n\n {\"id\":\"dragon-heir\",\"name\":\"龙的传人\",\"nameEn\":\"Dragon Heir\",\"author\":\"dsh-web-ui\",\"tagline\":\"不屈龙魂 · 万里长城双主题 · 朱砂龙印\",\"description\":\"龙的传人 — 一面是不屈龙魂(墨龙穿云、朱砂印章、不屈锋芒),一面是万里长城(青黛山色、金晖镀墙、苍茫暮色)。亮暗主题各自配一幅画与一枚龙印 favicon,面板半透明磨砂,让画透出来。\",\"tags\":[\"dragon\",\"loong\",\"chinese\",\"ink-wash\",\"great-wall\",\"dual-theme\"],\"accent\":\"#c3272b\",\"bodyAttr\":\"data-dsh-dragon-heir\",\"package\":\"@linxin666/dsh-client-ui-skin-dragon-heir\",\"order\":5},\n\n {\"id\":\"minecraft\",\"name\":\"Minecraft 方块世界\",\"nameEn\":\"Minecraft Voxel\",\"author\":\"dsh-web-ui\",\"tagline\":\"动态全景天空盒 · 方块按钮 · 告示牌输入框\",\"description\":\"复刻《我的世界》主界面氛围的方块皮肤:程序化绘制的像素全景天空盒(方块山、像素云、方块树、草方块地面)在身后缓慢旋转,界面浮在石板上;按钮还原 MC 菜单按钮(灰石板、悬停变黄、按下下沉),输入框做成带钉子的木告示牌。\",\"tags\":[\"minecraft\",\"voxel\",\"pixel\",\"game\",\"panorama\",\"skybox\"],\"accent\":\"#7cbd4b\",\"bodyAttr\":\"data-dsh-minecraft\",\"package\":\"@linxin666/dsh-client-ui-skin-minecraft\",\"order\":6},\n\n {\"id\":\"whale-song\",\"name\":\"鲸吟\",\"nameEn\":\"Whale Song\",\"author\":\"dsh-web-ui\",\"tagline\":\"深海鲸语女神背景 · 冰蓝海洋调色板 · 金色细线点缀\",\"description\":\"《鲸吟》— 深海鲸语女神主题:无文字纯氛围背景画(蓝发女神与鲸群居左、冰蓝星座网格与金线点缀、右侧大量留白)垫在半透明面板之下,遮罩随亮/暗主题实时切换,冰蓝/浅青/深海军蓝/钴蓝冷色体系重映射到全部 dsh token,暗色变体为深海夜航调。\",\"tags\":[\"whale\",\"ocean\",\"ice-blue\",\"goddess\",\"art\",\"translucent\"],\"accent\":\"#4d8fd4\",\"bodyAttr\":\"data-dsh-whale-song\",\"package\":\"@linxin666/dsh-client-ui-skin-whale-song\",\"order\":7},\n\n]\n","/**\n * Try-on engine for the in-GUI skin center.\n *\n * A skin's client bundle is executed through the REAL module system, not a\n * shim and not eval: the host route `/api/skin-center/bundle/<id>` serves\n * the skin's prebuilt `lib/client.js` as a same-origin script (mirroring\n * the kernel's own defaultLoadBundle — see dsh-client-modules), and its\n * body calls `window.__ModuleLoader__.load({id, factory})`, which only\n * REGISTERS the factory. `window.__DSH_MODULES__.import(package)` (the\n * kernel's ClientModuleSystem, contract C5/C6) then materializes it — which\n * auto-injects the skin's CSS `<style data-plugin>` tag — and\n * `surface.apply(miniCtx)` mounts the skin exactly as the fiber system\n * would, returning a full disposer. That makes try-on and its teardown the\n * real code paths, with no CSP `unsafe-eval` dependence and no startup\n * cost: the ~700KB of embedded art base64 is only parsed when a skin is\n * actually tried on.\n *\n * Mutual exclusion: the GUI never hosts two skins at once. The currently\n * ACTIVE skin is owned by its own cordis fiber (its disposer is not\n * reachable), so try-on retracts the active skin's visual writes by recipe:\n * remove its body attribute (its stylesheet goes inert), clear the\n * body-level backdrop inline styles (blue-fantasy's whale art), detach only\n * known skin chrome body children (title/status bars marked `data-skin-chrome`\n * or carrying the skin's body attribute, leaving other plugins' portals and\n * toasts in place), and neutralize known global-rule leaks (xp's sidebar\n * taskbar/start). Everything is snapshotted and restored on exit in original\n * order. The active skin's own fiber is never touched, so exiting try-on\n * returns the page to exactly the pre-try-on state.\n *\n * A ghost MutationObserver may survive retraction (blue-fantasy re-writes\n * its backdrop on theme flips), so during try-on a neutralizing observer\n * re-clears the backdrop props whenever `data-ds-dark-theme` changes.\n */\n\nimport { SKIN_CENTER_ENTRIES, type SkinCenterEntry } from './generated/skins.ts'\n\n/** Body-level backdrop properties skins may write inline (blue-fantasy). */\nconst BACKDROP_PROPS = [\n 'background-image',\n 'background-position',\n 'background-size',\n 'background-attachment',\n 'background-repeat',\n] as const\n\n/**\n * Per-skin neutralization CSS: rules that hide visual leaks whose styles\n * are NOT scoped under the skin's body attribute (they live on app elements\n * the skin touches, so detaching chrome cannot remove them). Matched by\n * css-module class substring, which is stable across rebuilds.\n */\nconst NEUTRALIZE_CSS: Record<string, string> = {\n // xp styles its start button + taskbar strip in the sidebar footer with\n // top-level (unscoped) rules; its apply() removes them on dispose, but\n // during try-on its ghost observer would re-add them, so hide instead.\n xp: [\n `[data-pane='sidebar'] [class*='xpTaskbar']{background:transparent!important;border-top:none!important;box-shadow:none!important}`,\n `[data-pane='sidebar'] [class*='xpStart']{display:none!important}`,\n ].join(''),\n}\n\n/** The window surfaces the boot protocol installs (manifest.ts contract). */\ninterface SkinCenterWindow {\n __DSH_BOOT__?: { entries?: Array<{ id: string }> }\n __DSH_MODULES__?: {\n import(specifier: string): Promise<unknown>\n invalidate(id: string): void\n }\n}\n\n/** Host base path of the skin bundle route (registered by src/routes.ts). */\nconst BUNDLE_ROUTE = '/api/skin-center/bundle'\n\n/**\n * Execute one skin's client bundle as a real same-origin script, mirroring\n * the kernel's own defaultLoadBundle (dsh-client-modules): the script body\n * calls `window.__ModuleLoader__.load({id, factory})`, which only registers\n * the factory — materialization is the caller's separate `import` step. No\n * eval: try-on works under any CSP that allows same-origin scripts (the\n * shell itself loads plugin bundles this way), and a failed fetch rejects\n * so the caller can restore the active skin instead of leaving it retracted.\n * @param url - same-origin bundle URL.\n * @returns a promise resolving once the script executed.\n */\nfunction loadBundleScript(url: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const el = document.createElement('script')\n el.async = true\n el.src = url\n el.addEventListener('load', () => {\n el.remove()\n resolve()\n }, { once: true })\n el.addEventListener('error', () => {\n el.remove()\n reject(new Error(`skin-center: bundle script ${url} failed to load`))\n }, { once: true })\n document.head.append(el)\n })\n}\n\n/** Read the page's composed boot-graph entry ids (only enabled plugins appear). */\nfunction bootEntryIds(): string[] {\n const boot = (window as SkinCenterWindow).__DSH_BOOT__\n return boot?.entries?.map(entry => entry.id) ?? []\n}\n\n/** The skin package currently ACTIVE in the boot graph, if it is one of ours. */\nexport function activeSkinEntry(): SkinCenterEntry | undefined {\n const ids = new Set(bootEntryIds())\n return SKIN_CENTER_ENTRIES.find(entry => ids.has(entry.package))\n}\n\n/**\n * Whether a direct body child is skin chrome owned by `skin`: marked with the\n * `data-skin-chrome` marker (minecraft/dragon-heir) or carrying the skin's\n * scoping body attribute. Everything else — other plugins' portals, toasts and\n * overlays appended to body — is left alone.\n */\nfunction isSkinChrome(el: Element, skin: SkinCenterEntry | null): boolean {\n if (el.hasAttribute('data-skin-chrome')) return true\n return skin !== null && el.hasAttribute(skin.bodyAttr)\n}\n\n/** Minimal ctx the skins' apply() needs: cordis effect lifecycle only. */\ninterface MiniCtx {\n effect(callback: () => () => void, label?: string): () => void\n /**\n * Service reads are answered with undefined: try-on is a pure-DOM stage,\n * so optional service access (e.g. ths reading the connection handle)\n * degrades to its fallback instead of throwing mid-apply.\n */\n get(key: string): unknown\n /** Hidden handle for the controller: run the disposers in reverse order. */\n __disposeAll(): void\n}\n\nfunction miniCtx(): MiniCtx {\n const disposers: Array<() => void> = []\n return {\n effect(callback) {\n disposers.push(callback())\n return () => {}\n },\n get() {\n return undefined\n },\n __disposeAll(): void {\n for (const dispose of disposers.reverse()) dispose()\n },\n }\n}\n\n/** Snapshot of the active skin's visual writes, restored on try-on exit. */\ninterface ActiveVisuals {\n skin: SkinCenterEntry | null\n /** Attribute value before retraction (null = attribute absent). */\n bodyAttr: string | null\n /** body inline style before retraction (null = none). */\n bodyStyle: string | null\n /** Skin chrome elements detached from body, re-inserted at their anchors. */\n detached: Array<{ el: HTMLElement; anchor: Node | null }>\n /** Neutralizes ghost backdrop writes while a try-on is live. */\n clearObserver: MutationObserver | null\n /** Hides global-rule leaks of the active skin (xp taskbar). */\n neutralizeStyle: HTMLStyleElement | null\n}\n\n/**\n * One live try-on session: owns the tried-on skin's disposer plus the\n * captured active-skin visuals, and restores everything on exit.\n */\nexport class TryOnController {\n private session: {\n /** The tried-on skin, or null when trying on the official stock look. */\n entry: SkinCenterEntry | null\n dispose: () => void\n active: ActiveVisuals\n } | null = null\n\n /**\n * Generation counter. A newer try-on or exit increments it, so an in-flight\n * `tryOn` (awaiting the real bundle load) can detect it was superseded and\n * drop only what it mounted instead of clobbering the newer session.\n */\n private epoch = 0\n\n /**\n * Loads one skin's client bundle so its factory registers on the page's\n * `__ModuleLoader__`. Defaults to a same-origin script tag from the host\n * route `/api/skin-center/bundle/<id>`; tests inject a stub.\n */\n private readonly loadBundle: (entry: SkinCenterEntry) => Promise<void>\n\n constructor(options: { loadBundle?: (entry: SkinCenterEntry) => Promise<void> } = {}) {\n this.loadBundle = options.loadBundle ?? (entry => loadBundleScript(`${BUNDLE_ROUTE}/${encodeURIComponent(entry.id)}`))\n }\n /** The skin currently being tried on, if any. */\n get trying(): SkinCenterEntry | null {\n return this.session?.entry ?? null\n }\n\n /** Whether the official stock look (no skin) is being tried on. */\n get tryingOfficial(): boolean {\n return this.session !== null && this.session.entry === null\n }\n\n /** Start trying on `entry` (replaces any live session). */\n async tryOn(entry: SkinCenterEntry): Promise<void> {\n if (entry.package === activeSkinEntry()?.package) return\n this.exit()\n const epoch = ++this.epoch\n\n const active: ActiveVisuals = this.captureAndRetractActive()\n let dispose: (() => void) | undefined\n try {\n dispose = await this.loadAndApply(entry)\n } catch (error) {\n if (epoch === this.epoch) this.restoreActive(active)\n throw error\n }\n if (epoch !== this.epoch) {\n // Superseded while loading (a newer try-on or exit started): drop only\n // what this attempt mounted — the newer operation owns the surface and\n // captured the active-skin visuals it needs on exit.\n this.cleanupModule(entry)\n dispose()\n return\n }\n this.session = { entry, dispose, active }\n }\n\n /**\n * Try on the official stock look: retract the active skin's visual writes\n * (same recipe as a skin try-on) and mount nothing. Exiting restores the\n * active skin exactly like any other try-on session.\n */\n tryOnOfficial(): void {\n if (activeSkinEntry() === null) return\n this.exit()\n this.epoch += 1\n const active: ActiveVisuals = this.captureAndRetractActive()\n this.session = { entry: null, dispose: () => {}, active }\n }\n\n /** Exit the live session: dispose the tried-on skin, then restore the active skin. */\n exit(): void {\n const session = this.session\n if (session === null) return\n this.epoch += 1\n this.session = null\n session.dispose()\n if (session.entry !== null) this.cleanupModule(session.entry)\n this.restoreActive(session.active)\n }\n\n /** Execute + materialize + mount the target skin through the real loader. */\n private async loadAndApply(entry: SkinCenterEntry): Promise<() => void> {\n const modules = (window as SkinCenterWindow).__DSH_MODULES__\n if (modules === undefined) throw new Error('skin-center: window.__DSH_MODULES__ missing')\n // This try-on session owns the module record for the package for its\n // whole life (a try-on of the ACTIVE skin is rejected above, and the GUI\n // never hosts two skins at once). Drop any factory a crashed earlier\n // session left behind so the script below registers a fresh one — the\n // duplicate-registration retry dance is unnecessary with the real\n // loader, whose load() throws on a duplicate id.\n modules.invalidate(entry.package)\n await this.loadBundle(entry)\n const surface = await modules.import(entry.package)\n const apply = (surface as { apply?: (ctx: unknown) => unknown }).apply\n if (typeof apply !== 'function') {\n throw new Error(`skin-center: \"${entry.package}\" client bundle exports no apply`)\n }\n const ctx = miniCtx()\n try {\n apply(ctx)\n } catch (error) {\n // A skin that throws mid-apply leaves partial writes behind (body\n // attribute, chrome, style tag) and never registers its disposer —\n // the tryOn catch only restores the ACTIVE skin. Roll the residue back\n // here so a crashed try-on can never bleed into the next one.\n this.cleanupModule(entry)\n document.body.removeAttribute(entry.bodyAttr)\n for (const el of [...document.body.children] as HTMLElement[]) {\n if (isSkinChrome(el, entry)) el.remove()\n }\n throw error\n }\n return ctx.__disposeAll\n }\n\n /** Drop the tried-on module record + its injected style tag. */\n private cleanupModule(entry: SkinCenterEntry): void {\n const modules = (window as SkinCenterWindow).__DSH_MODULES__\n modules?.invalidate(entry.package)\n for (const el of document.querySelectorAll(`style[data-plugin=${JSON.stringify(entry.package)}]`)) {\n el.remove()\n }\n }\n\n /**\n * Snapshot the active skin's visual writes and retract them so the tried-on\n * skin can take over the whole surface.\n */\n private captureAndRetractActive(): ActiveVisuals {\n const skin = activeSkinEntry() ?? null\n const body = document.body\n const bodyAttr = skin === null ? null : body.getAttribute(skin.bodyAttr)\n if (skin !== null && bodyAttr !== null) body.removeAttribute(skin.bodyAttr)\n\n const bodyStyle = body.getAttribute('style')\n for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop)\n\n // Detach only known skin chrome (marker/bodyAttr), leaving other\n // plugins' portals/toasts in place; remember each element's next sibling\n // so restore re-inserts in the original order.\n const children = [...body.children] as HTMLElement[]\n const chrome = new Set<HTMLElement>()\n for (const el of children) {\n if (el.id !== 'root' && isSkinChrome(el, skin)) chrome.add(el)\n }\n const detached: Array<{ el: HTMLElement; anchor: Node | null }> = []\n for (let i = 0; i < children.length; i++) {\n const el = children[i]\n if (!chrome.has(el)) continue\n let anchor: Node | null = null\n for (let j = i + 1; j < children.length; j++) {\n if (!chrome.has(children[j])) { anchor = children[j]; break }\n }\n detached.push({ el, anchor })\n }\n for (const { el } of detached) el.remove()\n\n // Neutralize a surviving ghost observer (blue-fantasy's backdrop\n // re-writer) across theme flips during the try-on.\n const clearObserver = new MutationObserver(() => {\n for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop)\n })\n clearObserver.observe(body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })\n\n const neutralizeCss = skin === null ? undefined : NEUTRALIZE_CSS[skin.id]\n const neutralizeStyle = neutralizeCss === undefined ? null : this.injectStyle(neutralizeCss)\n\n return { skin, bodyAttr, bodyStyle, detached, clearObserver, neutralizeStyle }\n }\n\n /** Restore the active skin's captured visual state. */\n private restoreActive(active: ActiveVisuals): void {\n const body = document.body\n if (active.skin !== null && active.bodyAttr !== null) {\n body.setAttribute(active.skin.bodyAttr, active.bodyAttr)\n }\n if (active.bodyStyle !== null) {\n body.setAttribute('style', active.bodyStyle)\n } else {\n body.removeAttribute('style')\n }\n for (const { el, anchor } of active.detached) {\n body.insertBefore(el, anchor !== null && anchor.parentNode === body ? anchor : null)\n }\n active.clearObserver?.disconnect()\n active.neutralizeStyle?.remove()\n }\n\n private injectStyle(css: string): HTMLStyleElement {\n const tag = document.createElement('style')\n tag.dataset.skinCenterNeutralize = ''\n tag.textContent = css\n document.head.append(tag)\n return tag\n }\n}\n","/**\n * The skin-center plugin card: one disclosure card inside the Web UI plugin\n * group (插件配置 → Web UI 插件), listing every installed skin plus the\n * official stock look. Live try-on executes the real bundle inside the GUI\n * (light/dark preview, full restore on exit); Apply is one click — the host\n * half runs `dsh-skin use` through /api/skin-center/apply, the config\n * watcher hot-reloads the patch, and the page reloads into the new skin.\n * Copy rides the standard `t` seat; the theme preview control drives the\n * official theme service (persisted, same as the Appearance row).\n */\nimport { useState, useSyncExternalStore, type ReactNode } from 'react'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'\nimport { SKIN_CENTER_ENTRIES, type SkinCenterEntry } from './generated/skins.ts'\nimport { activeSkinEntry, TryOnController } from './try-on.ts'\nimport css from './skin-center.module.css'\n\n/** Business face the skin-center apply() injects into the card. */\nexport interface SkinCenterInjected {\n controller: TryOnController\n theme: {\n getTheme(): ThemeSnapshot\n subscribe(listener: () => void): () => void\n setTheme(id: 'light' | 'dark'): void\n }\n}\n\n/** Plugin-card component props: group-item runtime share + locale seat + injected face. */\nexport type SkinCenterComponentProps =\n PropsRuntime<'web-ui.plugin.item'> & PropsLocale<'skinCenter'> & SkinCenterInjected\n\n/** The apply target of the official stock-look card. */\nconst OFFICIAL = 'official'\n\n/**\n * Render the skin-center card: a disclosure header naming the plugin, with\n * the skin list (official default + every installed skin; try-on / theme\n * preview / one-click apply) inside its body.\n * @param props - card props.\n * @returns the plugin card.\n */\nexport function SkinCenter({ t, controller, theme }: SkinCenterComponentProps) {\n const snapshot = useSyncExternalStore(theme.subscribe, theme.getTheme)\n const activePackage = activeSkinEntry()?.package\n const [open, setOpen] = useState(false)\n const [tryingId, setTryingId] = useState<string | null>(null)\n const [tryingOfficial, setTryingOfficial] = useState(false)\n const [applying, setApplying] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n\n const tryOn = (entry: SkinCenterEntry): void => {\n setError(null)\n void controller.tryOn(entry)\n .then(() => {\n setTryingId(entry.id)\n setTryingOfficial(false)\n })\n .catch(() => {\n // The controller may have torn down a previous session before the\n // load failed; reset both flags so no stale \"trying on\" lingers.\n setError(t('tryOnError'))\n setTryingId(null)\n setTryingOfficial(false)\n })\n }\n\n const tryOnOfficial = (): void => {\n setError(null)\n try {\n controller.tryOnOfficial()\n } catch {\n setError(t('tryOnError'))\n setTryingOfficial(false)\n return\n }\n setTryingId(null)\n setTryingOfficial(true)\n }\n\n const exitTryOn = (): void => {\n controller.exit()\n setTryingId(null)\n setTryingOfficial(false)\n }\n\n /**\n * Poll the host state until the config watcher reports the target active\n * (the patch write lands before the watcher re-applies it), or time out.\n * @param target - skin id, or `official` for the stock look.\n * @returns whether the target became active within the poll budget.\n */\n const confirmActive = (target: string): Promise<boolean> =>\n new Promise(resolve => {\n const expected = target === OFFICIAL ? 'none' : target\n let tries = 0\n const tick = (): void => {\n tries += 1\n void fetch('/api/skin-center/state')\n .then(async response => {\n const payload = await response.json().catch(() => null) as { ok?: boolean; active?: string } | null\n if (response.ok && payload?.ok === true && payload.active === expected) {\n resolve(true)\n return\n }\n if (tries >= 20) resolve(false)\n else window.setTimeout(tick, 250)\n })\n .catch(() => {\n if (tries >= 20) resolve(false)\n else window.setTimeout(tick, 250)\n })\n }\n tick()\n })\n\n /**\n * One-click apply: the host half runs `dsh-skin use <target>` (or\n * `use official`), the config watcher hot-reloads the patch within\n * seconds, then this page reloads to pick up the new boot graph.\n * @param target - skin id, or `official` for the stock look.\n */\n const applySkin = (target: string): void => {\n setError(null)\n setApplying(target)\n const body = target === OFFICIAL ? { official: true } : { skin: target }\n void fetch('/api/skin-center/apply', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n .then(async response => {\n const payload = await response.json().catch(() => null) as { ok?: boolean; error?: string } | null\n if (!response.ok || payload?.ok !== true) {\n throw new Error(payload?.error ?? `HTTP ${response.status}`)\n }\n setApplying(null)\n // Patch written; reload only once the watcher reports the target\n // active, so the page never boots into the old skin.\n void confirmActive(target).then(confirmed => {\n if (confirmed) {\n window.location.reload()\n } else {\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('appliedUnconfirmed')} — ${command}`)\n }\n })\n })\n .catch((cause: unknown) => {\n setApplying(null)\n const detail = cause instanceof Error ? cause.message : String(cause)\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('applyFailed')} (${detail}) — ${command}`)\n })\n }\n\n const dark = snapshot.active.colorScheme === 'dark'\n\n /** One row: try-on control + apply button. Shared by the official card and every skin card. */\n const actionButtons = (opts: {\n key: string\n isActive: boolean\n isTrying: boolean\n onTryOn: () => void\n applyLabel: string\n }): ReactNode => (\n <div className={css.actions}>\n {opts.isActive ? (\n <button type=\"button\" className={`${css.button} ${css.buttonGhost}`} disabled>\n {t('tryOn')}\n </button>\n ) : opts.isTrying ? (\n <button type=\"button\" className={`${css.button} ${css.buttonPrimary}`} onClick={exitTryOn}>\n {t('exitTryOn')}\n </button>\n ) : (\n <button type=\"button\" className={`${css.button} ${css.buttonPrimary}`} onClick={opts.onTryOn}>\n {t('tryOn')}\n </button>\n )}\n <button\n type=\"button\"\n className={css.button}\n disabled={applying !== null}\n onClick={() => { applySkin(opts.key) }}\n >\n {applying === opts.key ? t('applying') : opts.applyLabel}\n </button>\n </div>\n )\n\n return (\n <li className={css.pluginCard}>\n <button\n type=\"button\"\n className={css.cardHeader}\n aria-expanded={open}\n aria-label={`${t(open ? 'collapse' : 'expand')}: ${t('title')}`}\n onClick={() => { setOpen(current => !current) }}\n >\n <span className={css.headText}>\n <span className={css.pluginName}>\n {t('title')}\n <span className={css.titleBadge}>{String(SKIN_CENTER_ENTRIES.length)}</span>\n </span>\n <span className={css.cardDescription} title={t('cardDescription')}>{t('cardDescription')}</span>\n </span>\n <span className={open ? css.chevronOpen : css.chevron}>▾</span>\n </button>\n\n {open\n ? (\n <div className={css.cardBody}>\n <div className={css.head}>\n <div className={css.intro} title={t('intro')}>{t('intro')}</div>\n <div className={css.themeRow}>\n <span className={css.themeLabel}>{t('theme')}</span>\n <button\n type=\"button\"\n className={`${css.themeButton} ${dark ? '' : css.themeButtonActive}`}\n onClick={() => { theme.setTheme('light') }}\n >\n {t('themeLight')}\n </button>\n <button\n type=\"button\"\n className={`${css.themeButton} ${dark ? css.themeButtonActive : ''}`}\n onClick={() => { theme.setTheme('dark') }}\n >\n {t('themeDark')}\n </button>\n </div>\n </div>\n\n {error !== null && <div className={css.error}>{error}</div>}\n\n <div className={css.list}>\n {(() => {\n const isActive = activePackage === undefined\n const isTrying = tryingOfficial\n const badge = isActive ? t('active') : isTrying ? t('tryingOn') : null\n return (\n <div className={css.card} key={OFFICIAL}>\n <div className={css.cardHead}>\n <span className={css.swatch} style={{ background: '#98a1ab' }} aria-hidden=\"true\" />\n <span className={css.cardName} title={t('official')}>{t('official')}</span>\n {badge !== null && (\n <span className={`${css.badge} ${isActive ? css.badgeActive : css.badgeTrying}`}>\n {badge}\n </span>\n )}\n </div>\n <div className={css.cardTagline} title={t('officialTagline')}>{t('officialTagline')}</div>\n {actionButtons({\n key: OFFICIAL,\n isActive,\n isTrying,\n onTryOn: tryOnOfficial,\n applyLabel: t('restore'),\n })}\n </div>\n )\n })()}\n\n {SKIN_CENTER_ENTRIES.map(entry => {\n const isActive = entry.package === activePackage\n const isTrying = entry.id === tryingId\n const badge = isActive ? t('active') : isTrying ? t('tryingOn') : null\n return (\n <div className={css.card} key={entry.id}>\n <div className={css.cardHead}>\n <span className={css.swatch} style={{ background: entry.accent }} aria-hidden=\"true\" />\n <span className={css.cardName} title={entry.nameEn}>{entry.nameEn}</span>\n {badge !== null && (\n <span className={`${css.badge} ${isActive ? css.badgeActive : css.badgeTrying}`}>\n {badge}\n </span>\n )}\n </div>\n <div className={css.cardTagline} title={entry.tagline}>{entry.tagline}</div>\n {actionButtons({\n key: entry.id,\n isActive,\n isTrying,\n onTryOn: () => { tryOn(entry) },\n applyLabel: t('apply'),\n })}\n </div>\n )\n })}\n </div>\n </div>\n )\n : null}\n </li>\n )\n}\n","/**\n * Skin-center locale dictionaries. The plugin-card name, its description,\n * and every control of the in-GUI skin center is localized through the\n * standard `t` seat.\n */\n\n/** Copy keys owned by this plugin. */\nexport type SkinCenterKey =\n | 'title'\n | 'cardDescription'\n | 'expand'\n | 'collapse'\n | 'intro'\n | 'official'\n | 'officialTagline'\n | 'active'\n | 'tryingOn'\n | 'tryOn'\n | 'exitTryOn'\n | 'apply'\n | 'applying'\n | 'restore'\n | 'applyFailed'\n | 'appliedUnconfirmed'\n | 'theme'\n | 'themeLight'\n | 'themeDark'\n | 'tryOnError'\n\nexport const en: Record<SkinCenterKey, string> = {\n title: 'Skin Center',\n cardDescription: 'Try on any installed skin live in the GUI — exit restores instantly, applying persists in one click.',\n expand: 'Expand',\n collapse: 'Collapse',\n intro: 'Try on any skin live — it takes effect instantly, exit restores the current look. Apply persists it across restarts.',\n official: 'Official default',\n officialTagline: 'The stock DSH look with no skin applied.',\n active: 'Active',\n tryingOn: 'Trying on',\n tryOn: 'Try on',\n exitTryOn: 'Exit try-on',\n apply: 'Apply',\n applying: 'Applying…',\n restore: 'Restore',\n applyFailed: 'Apply failed',\n appliedUnconfirmed: 'Applied, but the change has not been confirmed — refresh the page if the skin did not switch',\n theme: 'Theme preview',\n themeLight: 'Light',\n themeDark: 'Dark',\n tryOnError: 'Try-on failed — see console',\n}\n\nexport const zh: Record<SkinCenterKey, string> = {\n title: '皮肤中心',\n cardDescription: '在 GUI 内即时试穿任意皮肤,退出即完全还原;应用一键完成并自动刷新。',\n expand: '展开',\n collapse: '收起',\n intro: '任意皮肤可即时试穿,退出即完全还原;「应用」一键持久化,页面自动刷新生效。',\n official: '官方默认',\n officialTagline: '还原 DSH 官方默认外观,不应用任何皮肤。',\n active: '当前激活',\n tryingOn: '试穿中',\n tryOn: '试穿',\n exitTryOn: '退出试穿',\n apply: '应用',\n applying: '应用中…',\n restore: '恢复默认',\n applyFailed: '应用失败',\n appliedUnconfirmed: '已写入配置但尚未确认生效——若皮肤未切换请手动刷新页面',\n theme: '主题预览',\n themeLight: '亮色',\n themeDark: '暗色',\n tryOnError: '试穿失败,详见控制台',\n}\n","/**\n * In-GUI skin center, browser half: registers the Skins plugin card into the\n * Web UI plugin group (`web-ui.plugin.item`, declared by the web-ui-settings\n * group card under 插件配置) and provides the try-on controller + official\n * theme handle to it. The card lists every installed skin (embedded\n * registry), tries it on live inside the GUI, exits with a full restore, and\n * copies the one-command apply. The plugin writes only DOM and the settings\n * ledger — no services, no events, no model access.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ThemeRuntime } from '@deepseek-ai/dsh-client-ui-theme/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport { SkinCenter, type SkinCenterInjected } from './SkinCenter.tsx'\nimport { en, zh, type SkinCenterKey } from './locales.ts'\nimport { TryOnController } from './try-on.ts'\n\nexport type { SkinCenterComponentProps, SkinCenterInjected } from './SkinCenter.tsx'\nexport { TryOnController } from './try-on.ts'\n\n/** Locale namespace owned by this plugin. */\nexport const NS = 'skinCenter'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The skin-center card's copy. */\n skinCenter: SkinCenterKey\n }\n\n interface SlotMap {\n /**\n * The child slot the Web UI plugin group declares; this card registers\n * into the group instead of the top-level `settings.plugin.item` list.\n * Spelled here with the same shape so this package can register without\n * depending on the sibling UI package.\n */\n 'web-ui.plugin.item': { kind: 'list'; scope: 'root'; owner: SettingsPluginItemOwnerProps }\n }\n}\n\n/** Owner share of a plugin card (the group card supplies nothing). */\nexport interface SettingsPluginItemOwnerProps {\n /** Marker field: card owner props are intentionally empty. */\n children?: never\n}\n\n/** Required services: slots + locale (plugin card) and theme (preview toggle). */\nexport const inject = ['slots', 'locale', 'theme']\n\n/**\n * Register the skin-center dictionaries, the body scope attribute, and the\n * Skins plugin card inside the Web UI plugin group.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-skin-center: dictionaries')\n\n // The card's own styles scope under this attribute so they keep applying\n // during try-on (when the active skin's attribute is retracted).\n ctx.effect(() => {\n document.body.dataset.dshSkinCenter = ''\n return () => { delete document.body.dataset.dshSkinCenter }\n }, 'ui-skin-center: body scope')\n\n const theme = ctx.get('theme') as ThemeRuntime\n const controller = new TryOnController()\n const injected = (): SkinCenterInjected => ({\n controller,\n theme: {\n getTheme: () => theme.getTheme(),\n subscribe: listener => ctx.on('theme/change', listener),\n setTheme: id => theme.setTheme(id),\n },\n })\n\n ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({\n name: 'web-ui.plugin.item',\n id: 'skins',\n order: 110,\n locale: NS,\n inject: injected,\n }, SkinCenter))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["css"],"sources":["../../../packages/skins/skin-center/src/client/generated/skins.ts","../../../packages/skins/skin-center/src/client/try-on.ts","../../../packages/skins/skin-center/src/client/SkinCenter.tsx","../../../packages/skins/skin-center/src/client/background.ts","../../../packages/skins/skin-center/src/client/locales.ts","../../../packages/skins/skin-center/src/client/index.ts"],"sourcesContent":["/**\n * AUTO-GENERATED by scripts/skin-center-bundles — do not edit by hand.\n * Skin registry for the GUI skin center: gallery metadata only. Client\n * bundle text is NOT embedded here — try-on fetches each skin's\n * prebuilt lib/client.js on demand from the host route\n * /api/skin-center/bundle/<id> (a same-origin script executed by the\n * page's own module loader; no eval, and the GUI never parses the\n * embedded art base64 at startup).\n * Regenerate with `node scripts/skin-center-bundles`.\n */\n\n/** One registry entry: gallery metadata for the skin center card. */\nexport interface SkinCenterEntry {\n /** Skin id (packages/skins/<id>/). */\n id: string\n /** Display name (zh). */\n name: string\n /** Display name (en). */\n nameEn: string\n /** One-line tagline. */\n tagline: string\n /** Accent color for the swatch. */\n accent: string\n /** Author credit. */\n author?: string\n /** Longer description. */\n description?: string\n /** Tag list. */\n tags?: string[]\n /** Body attribute that scopes the skin styles. */\n bodyAttr: string\n /** Plugin package name (the boot-graph entry id when active). */\n package: string\n /** Gallery sort order (packages/skins/<name>/skin.json). */\n order?: number\n}\n\n/** Every skin, ordered by packages/skins/<name>/skin.json `order`. */\nexport const SKIN_CENTER_ENTRIES: readonly SkinCenterEntry[] = [\n {\"id\":\"qq98\",\"name\":\"QQ2008 怀旧版\",\"nameEn\":\"QQ2008 Retro\",\"author\":\"dsh-web-ui\",\"tagline\":\"水晶蓝桌面 · 玻璃深蓝标题栏 · 戴围巾企鹅\",\"description\":\"dsh web ui 家族收录的第一个皮肤:QQ2008 水晶蓝年代。深蓝渐变桌面、玻璃质感标题栏、浅蓝状态栏和圆角高光控件,配一只戴围巾的企鹅。\",\"tags\":[\"retro\",\"qq\",\"2008\",\"crystal-blue\",\"nostalgia\"],\"accent\":\"#2b7cd9\",\"bodyAttr\":\"data-dsh-retro\",\"package\":\"@linxin666/dsh-client-ui-skin-qq98\",\"order\":1},\n\n {\"id\":\"ths\",\"name\":\"同花顺风格\",\"nameEn\":\"Tonghuashun Trading\",\"author\":\"dsh-web-ui\",\"tagline\":\"品牌红标题栏 · 实时行情状态栏 · 灰蓝数据终端\",\"description\":\"同花顺风格炒股主题:品牌红标题栏带上证指数行情签,状态栏红涨绿跌,自选股风格的侧边栏和交易终端面板,写代码也像盯盘。\",\"tags\":[\"stock\",\"trading\",\"terminal\",\"red\"],\"accent\":\"#e60012\",\"bodyAttr\":\"data-dsh-ths\",\"package\":\"@linxin666/dsh-client-ui-skin-ths\",\"order\":2},\n\n {\"id\":\"xp\",\"name\":\"Windows XP (Luna)\",\"nameEn\":\"Windows XP Luna\",\"author\":\"dsh-web-ui\",\"tagline\":\"Luna 蓝窗口条 · 绿色开始按钮 · Bliss 蓝天桌面\",\"description\":\"Windows XP (Luna) 复古主题:蓝色渐变窗口条带窗口按钮、米色状态栏(大写/数字/滚动指示灯)、侧边栏任务栏上的绿色「开始」按钮、资源管理器风格树行和 Bliss 蓝天桌面,全局直角。\",\"tags\":[\"retro\",\"xp\",\"luna\",\"windows\",\"start-button\"],\"accent\":\"#316ac5\",\"bodyAttr\":\"data-dsh-xp\",\"package\":\"@linxin666/dsh-client-ui-skin-xp\",\"order\":3},\n\n {\"id\":\"blue-fantasy\",\"name\":\"蓝色幻想\",\"nameEn\":\"Blue Fantasy\",\"author\":\"powerdog996(DreamSkin 社区)· dsh-web-ui 适配\",\"tagline\":\"鲸鱼插画背景 · periwinkle 靛蓝调色板 · 半透明面板\",\"description\":\"DreamSkin「DeepSeek-鲸鱼娘」Codex 桌面主题的 dsh 适配:鲸鱼插画背景垫在半透明面板之下,遮罩随亮/暗主题实时切换,periwinkle 靛蓝色调重映射到全部 dsh token。\",\"tags\":[\"dreamskin\",\"whale\",\"indigo\",\"art\",\"translucent\"],\"accent\":\"#4a5fa8\",\"bodyAttr\":\"data-dsh-blue-fantasy\",\"package\":\"@linxin666/dsh-client-ui-skin-blue-fantasy\",\"order\":4},\n\n {\"id\":\"dragon-heir\",\"name\":\"龙的传人\",\"nameEn\":\"Dragon Heir\",\"author\":\"dsh-web-ui\",\"tagline\":\"不屈龙魂 · 万里长城双主题 · 朱砂龙印\",\"description\":\"龙的传人 — 一面是不屈龙魂(墨龙穿云、朱砂印章、不屈锋芒),一面是万里长城(青黛山色、金晖镀墙、苍茫暮色)。亮暗主题各自配一幅画与一枚龙印 favicon,面板半透明磨砂,让画透出来。\",\"tags\":[\"dragon\",\"loong\",\"chinese\",\"ink-wash\",\"great-wall\",\"dual-theme\"],\"accent\":\"#c3272b\",\"bodyAttr\":\"data-dsh-dragon-heir\",\"package\":\"@linxin666/dsh-client-ui-skin-dragon-heir\",\"order\":5},\n\n {\"id\":\"minecraft\",\"name\":\"Minecraft 方块世界\",\"nameEn\":\"Minecraft Voxel\",\"author\":\"dsh-web-ui\",\"tagline\":\"动态全景天空盒 · 方块按钮 · 告示牌输入框\",\"description\":\"复刻《我的世界》主界面氛围的方块皮肤:程序化绘制的像素全景天空盒(方块山、像素云、方块树、草方块地面)在身后缓慢旋转,界面浮在石板上;按钮还原 MC 菜单按钮(灰石板、悬停变黄、按下下沉),输入框做成带钉子的木告示牌。\",\"tags\":[\"minecraft\",\"voxel\",\"pixel\",\"game\",\"panorama\",\"skybox\"],\"accent\":\"#7cbd4b\",\"bodyAttr\":\"data-dsh-minecraft\",\"package\":\"@linxin666/dsh-client-ui-skin-minecraft\",\"order\":6},\n\n {\"id\":\"whale-song\",\"name\":\"鲸吟\",\"nameEn\":\"Whale Song\",\"author\":\"dsh-web-ui\",\"tagline\":\"深海鲸语女神背景 · 冰蓝海洋调色板 · 金色细线点缀\",\"description\":\"《鲸吟》— 深海鲸语女神主题:无文字纯氛围背景画(蓝发女神与鲸群居左、冰蓝星座网格与金线点缀、右侧大量留白)垫在半透明面板之下,遮罩随亮/暗主题实时切换,冰蓝/浅青/深海军蓝/钴蓝冷色体系重映射到全部 dsh token,暗色变体为深海夜航调。\",\"tags\":[\"whale\",\"ocean\",\"ice-blue\",\"goddess\",\"art\",\"translucent\"],\"accent\":\"#4d8fd4\",\"bodyAttr\":\"data-dsh-whale-song\",\"package\":\"@linxin666/dsh-client-ui-skin-whale-song\",\"order\":7},\n\n]\n","/**\n * Try-on engine for the in-GUI skin center.\n *\n * A skin's client bundle is executed through the REAL module system, not a\n * shim and not eval: the host route `/api/skin-center/bundle/<id>` serves\n * the skin's prebuilt `lib/client.js` as a same-origin script (mirroring\n * the kernel's own defaultLoadBundle — see dsh-client-modules), and its\n * body calls `window.__ModuleLoader__.load({id, factory})`, which only\n * REGISTERS the factory. `window.__DSH_MODULES__.import(package)` (the\n * kernel's ClientModuleSystem, contract C5/C6) then materializes it — which\n * auto-injects the skin's CSS `<style data-plugin>` tag — and\n * `surface.apply(miniCtx)` mounts the skin exactly as the fiber system\n * would, returning a full disposer. That makes try-on and its teardown the\n * real code paths, with no CSP `unsafe-eval` dependence and no startup\n * cost: the ~700KB of embedded art base64 is only parsed when a skin is\n * actually tried on.\n *\n * Mutual exclusion: the GUI never hosts two skins at once. The currently\n * ACTIVE skin is owned by its own cordis fiber (its disposer is not\n * reachable), so try-on retracts the active skin's visual writes by recipe:\n * remove its body attribute (its stylesheet goes inert), clear the\n * body-level backdrop inline styles (blue-fantasy's whale art), detach only\n * known skin chrome body children (title/status bars marked `data-skin-chrome`\n * or carrying the skin's body attribute, leaving other plugins' portals and\n * toasts in place), and neutralize known global-rule leaks (xp's sidebar\n * taskbar/start). Everything is snapshotted and restored on exit in original\n * order. The active skin's own fiber is never touched, so exiting try-on\n * returns the page to exactly the pre-try-on state.\n *\n * A ghost MutationObserver may survive retraction (blue-fantasy re-writes\n * its backdrop on theme flips), so during try-on a neutralizing observer\n * re-clears the backdrop props whenever `data-ds-dark-theme` changes.\n */\n\nimport { SKIN_CENTER_ENTRIES, type SkinCenterEntry } from './generated/skins.ts'\n\n/** Body-level backdrop properties skins may write inline (blue-fantasy). */\nconst BACKDROP_PROPS = [\n 'background-image',\n 'background-position',\n 'background-size',\n 'background-attachment',\n 'background-repeat',\n] as const\n\n/**\n * Per-skin neutralization CSS: rules that hide visual leaks whose styles\n * are NOT scoped under the skin's body attribute (they live on app elements\n * the skin touches, so detaching chrome cannot remove them). Matched by\n * css-module class substring, which is stable across rebuilds.\n */\nconst NEUTRALIZE_CSS: Record<string, string> = {\n // xp styles its start button + taskbar strip in the sidebar footer with\n // top-level (unscoped) rules; its apply() removes them on dispose, but\n // during try-on its ghost observer would re-add them, so hide instead.\n xp: [\n `[data-pane='sidebar'] [class*='xpTaskbar']{background:transparent!important;border-top:none!important;box-shadow:none!important}`,\n `[data-pane='sidebar'] [class*='xpStart']{display:none!important}`,\n ].join(''),\n}\n\n/** The window surfaces the boot protocol installs (manifest.ts contract). */\ninterface SkinCenterWindow {\n __DSH_BOOT__?: { entries?: Array<{ id: string }> }\n __DSH_MODULES__?: {\n import(specifier: string): Promise<unknown>\n invalidate(id: string): void\n }\n}\n\n/** Host base path of the skin bundle route (registered by src/routes.ts). */\nconst BUNDLE_ROUTE = '/api/skin-center/bundle'\n\n/**\n * Execute one skin's client bundle as a real same-origin script, mirroring\n * the kernel's own defaultLoadBundle (dsh-client-modules): the script body\n * calls `window.__ModuleLoader__.load({id, factory})`, which only registers\n * the factory — materialization is the caller's separate `import` step. No\n * eval: try-on works under any CSP that allows same-origin scripts (the\n * shell itself loads plugin bundles this way), and a failed fetch rejects\n * so the caller can restore the active skin instead of leaving it retracted.\n * @param url - same-origin bundle URL.\n * @returns a promise resolving once the script executed.\n */\nfunction loadBundleScript(url: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const el = document.createElement('script')\n el.async = true\n el.src = url\n el.addEventListener('load', () => {\n el.remove()\n resolve()\n }, { once: true })\n el.addEventListener('error', () => {\n el.remove()\n reject(new Error(`skin-center: bundle script ${url} failed to load`))\n }, { once: true })\n document.head.append(el)\n })\n}\n\n/** Read the page's composed boot-graph entry ids (only enabled plugins appear). */\nfunction bootEntryIds(): string[] {\n const boot = (window as SkinCenterWindow).__DSH_BOOT__\n return boot?.entries?.map(entry => entry.id) ?? []\n}\n\n/** The skin package currently ACTIVE in the boot graph, if it is one of ours. */\nexport function activeSkinEntry(): SkinCenterEntry | undefined {\n const ids = new Set(bootEntryIds())\n return SKIN_CENTER_ENTRIES.find(entry => ids.has(entry.package))\n}\n\n/**\n * Whether a direct body child is skin chrome owned by `skin`: marked with the\n * `data-skin-chrome` marker (minecraft/dragon-heir) or carrying the skin's\n * scoping body attribute. Everything else — other plugins' portals, toasts and\n * overlays appended to body — is left alone.\n */\nfunction isSkinChrome(el: Element, skin: SkinCenterEntry | null): boolean {\n if (el.hasAttribute('data-skin-chrome')) return true\n return skin !== null && el.hasAttribute(skin.bodyAttr)\n}\n\n/** Minimal ctx the skins' apply() needs: cordis effect lifecycle only. */\ninterface MiniCtx {\n effect(callback: () => () => void, label?: string): () => void\n /**\n * Service reads are answered with undefined: try-on is a pure-DOM stage,\n * so optional service access (e.g. ths reading the connection handle)\n * degrades to its fallback instead of throwing mid-apply.\n */\n get(key: string): unknown\n /** Hidden handle for the controller: run the disposers in reverse order. */\n __disposeAll(): void\n}\n\nfunction miniCtx(): MiniCtx {\n const disposers: Array<() => void> = []\n return {\n effect(callback) {\n disposers.push(callback())\n return () => {}\n },\n get() {\n return undefined\n },\n __disposeAll(): void {\n for (const dispose of disposers.reverse()) dispose()\n },\n }\n}\n\n/** Snapshot of the active skin's visual writes, restored on try-on exit. */\ninterface ActiveVisuals {\n skin: SkinCenterEntry | null\n /** Attribute value before retraction (null = attribute absent). */\n bodyAttr: string | null\n /** body inline style before retraction (null = none). */\n bodyStyle: string | null\n /** Skin chrome elements detached from body, re-inserted at their anchors. */\n detached: Array<{ el: HTMLElement; anchor: Node | null }>\n /** Neutralizes ghost backdrop writes while a try-on is live. */\n clearObserver: MutationObserver | null\n /** Hides global-rule leaks of the active skin (xp taskbar). */\n neutralizeStyle: HTMLStyleElement | null\n}\n\n/**\n * One live try-on session: owns the tried-on skin's disposer plus the\n * captured active-skin visuals, and restores everything on exit.\n */\nexport class TryOnController {\n private session: {\n /** The tried-on skin, or null when trying on the official stock look. */\n entry: SkinCenterEntry | null\n dispose: () => void\n active: ActiveVisuals\n } | null = null\n\n /**\n * Generation counter. A newer try-on or exit increments it, so an in-flight\n * `tryOn` (awaiting the real bundle load) can detect it was superseded and\n * drop only what it mounted instead of clobbering the newer session.\n */\n private epoch = 0\n\n /**\n * Loads one skin's client bundle so its factory registers on the page's\n * `__ModuleLoader__`. Defaults to a same-origin script tag from the host\n * route `/api/skin-center/bundle/<id>`; tests inject a stub.\n */\n private readonly loadBundle: (entry: SkinCenterEntry) => Promise<void>\n\n constructor(options: { loadBundle?: (entry: SkinCenterEntry) => Promise<void> } = {}) {\n this.loadBundle = options.loadBundle ?? (entry => loadBundleScript(`${BUNDLE_ROUTE}/${encodeURIComponent(entry.id)}`))\n }\n /** The skin currently being tried on, if any. */\n get trying(): SkinCenterEntry | null {\n return this.session?.entry ?? null\n }\n\n /** Whether the official stock look (no skin) is being tried on. */\n get tryingOfficial(): boolean {\n return this.session !== null && this.session.entry === null\n }\n\n /** Start trying on `entry` (replaces any live session). */\n async tryOn(entry: SkinCenterEntry): Promise<void> {\n if (entry.package === activeSkinEntry()?.package) return\n this.exit()\n const epoch = ++this.epoch\n\n const active: ActiveVisuals = this.captureAndRetractActive()\n let dispose: (() => void) | undefined\n try {\n dispose = await this.loadAndApply(entry)\n } catch (error) {\n if (epoch === this.epoch) this.restoreActive(active)\n throw error\n }\n if (epoch !== this.epoch) {\n // Superseded while loading (a newer try-on or exit started): drop only\n // what this attempt mounted — the newer operation owns the surface and\n // captured the active-skin visuals it needs on exit.\n this.cleanupModule(entry)\n dispose()\n return\n }\n this.session = { entry, dispose, active }\n }\n\n /**\n * Try on the official stock look: retract the active skin's visual writes\n * (same recipe as a skin try-on) and mount nothing. Exiting restores the\n * active skin exactly like any other try-on session.\n */\n tryOnOfficial(): void {\n if (activeSkinEntry() === null) return\n this.exit()\n this.epoch += 1\n const active: ActiveVisuals = this.captureAndRetractActive()\n this.session = { entry: null, dispose: () => {}, active }\n }\n\n /** Exit the live session: dispose the tried-on skin, then restore the active skin. */\n exit(): void {\n const session = this.session\n if (session === null) return\n this.epoch += 1\n this.session = null\n session.dispose()\n if (session.entry !== null) this.cleanupModule(session.entry)\n this.restoreActive(session.active)\n }\n\n /** Execute + materialize + mount the target skin through the real loader. */\n private async loadAndApply(entry: SkinCenterEntry): Promise<() => void> {\n const modules = (window as SkinCenterWindow).__DSH_MODULES__\n if (modules === undefined) throw new Error('skin-center: window.__DSH_MODULES__ missing')\n // This try-on session owns the module record for the package for its\n // whole life (a try-on of the ACTIVE skin is rejected above, and the GUI\n // never hosts two skins at once). Drop any factory a crashed earlier\n // session left behind so the script below registers a fresh one — the\n // duplicate-registration retry dance is unnecessary with the real\n // loader, whose load() throws on a duplicate id.\n modules.invalidate(entry.package)\n await this.loadBundle(entry)\n const surface = await modules.import(entry.package)\n const apply = (surface as { apply?: (ctx: unknown) => unknown }).apply\n if (typeof apply !== 'function') {\n throw new Error(`skin-center: \"${entry.package}\" client bundle exports no apply`)\n }\n const ctx = miniCtx()\n try {\n apply(ctx)\n } catch (error) {\n // A skin that throws mid-apply leaves partial writes behind (body\n // attribute, chrome, style tag) and never registers its disposer —\n // the tryOn catch only restores the ACTIVE skin. Roll the residue back\n // here so a crashed try-on can never bleed into the next one.\n this.cleanupModule(entry)\n document.body.removeAttribute(entry.bodyAttr)\n for (const el of [...document.body.children] as HTMLElement[]) {\n if (isSkinChrome(el, entry)) el.remove()\n }\n throw error\n }\n return ctx.__disposeAll\n }\n\n /** Drop the tried-on module record + its injected style tag. */\n private cleanupModule(entry: SkinCenterEntry): void {\n const modules = (window as SkinCenterWindow).__DSH_MODULES__\n modules?.invalidate(entry.package)\n for (const el of document.querySelectorAll(`style[data-plugin=${JSON.stringify(entry.package)}]`)) {\n el.remove()\n }\n }\n\n /**\n * Snapshot the active skin's visual writes and retract them so the tried-on\n * skin can take over the whole surface.\n */\n private captureAndRetractActive(): ActiveVisuals {\n const skin = activeSkinEntry() ?? null\n const body = document.body\n const bodyAttr = skin === null ? null : body.getAttribute(skin.bodyAttr)\n if (skin !== null && bodyAttr !== null) body.removeAttribute(skin.bodyAttr)\n\n const bodyStyle = body.getAttribute('style')\n for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop)\n\n // Detach only known skin chrome (marker/bodyAttr), leaving other\n // plugins' portals/toasts in place; remember each element's next sibling\n // so restore re-inserts in the original order.\n const children = [...body.children] as HTMLElement[]\n const chrome = new Set<HTMLElement>()\n for (const el of children) {\n if (el.id !== 'root' && isSkinChrome(el, skin)) chrome.add(el)\n }\n const detached: Array<{ el: HTMLElement; anchor: Node | null }> = []\n for (let i = 0; i < children.length; i++) {\n const el = children[i]\n if (!chrome.has(el)) continue\n let anchor: Node | null = null\n for (let j = i + 1; j < children.length; j++) {\n if (!chrome.has(children[j])) { anchor = children[j]; break }\n }\n detached.push({ el, anchor })\n }\n for (const { el } of detached) el.remove()\n\n // Neutralize a surviving ghost observer (blue-fantasy's backdrop\n // re-writer) across theme flips during the try-on.\n const clearObserver = new MutationObserver(() => {\n for (const prop of BACKDROP_PROPS) body.style.removeProperty(prop)\n })\n clearObserver.observe(body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })\n\n const neutralizeCss = skin === null ? undefined : NEUTRALIZE_CSS[skin.id]\n const neutralizeStyle = neutralizeCss === undefined ? null : this.injectStyle(neutralizeCss)\n\n return { skin, bodyAttr, bodyStyle, detached, clearObserver, neutralizeStyle }\n }\n\n /** Restore the active skin's captured visual state. */\n private restoreActive(active: ActiveVisuals): void {\n const body = document.body\n if (active.skin !== null && active.bodyAttr !== null) {\n body.setAttribute(active.skin.bodyAttr, active.bodyAttr)\n }\n if (active.bodyStyle !== null) {\n body.setAttribute('style', active.bodyStyle)\n } else {\n body.removeAttribute('style')\n }\n for (const { el, anchor } of active.detached) {\n body.insertBefore(el, anchor !== null && anchor.parentNode === body ? anchor : null)\n }\n active.clearObserver?.disconnect()\n active.neutralizeStyle?.remove()\n }\n\n private injectStyle(css: string): HTMLStyleElement {\n const tag = document.createElement('style')\n tag.dataset.skinCenterNeutralize = ''\n tag.textContent = css\n document.head.append(tag)\n return tag\n }\n}\n","/**\n * The skin-center plugin card: one disclosure card inside the Web UI plugin\n * group (插件配置 → Web UI 插件), listing every installed skin plus the\n * official stock look. Live try-on executes the real bundle inside the GUI\n * (light/dark preview, full restore on exit); Apply is one click — the host\n * half runs `dsh-skin use` through /api/skin-center/apply, the config\n * watcher hot-reloads the patch, and the page reloads into the new skin.\n * Copy rides the standard `t` seat; the theme preview control drives the\n * official theme service (persisted, same as the Appearance row).\n */\nimport { useState, useSyncExternalStore, type ReactNode } from 'react'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'\nimport { SKIN_CENTER_ENTRIES, type SkinCenterEntry } from './generated/skins.ts'\nimport type { SkinBackgroundHandle } from './background.ts'\nimport { activeSkinEntry, TryOnController } from './try-on.ts'\nimport css from './skin-center.module.css'\n\n/** Business face the skin-center apply() injects into the card. */\nexport interface SkinCenterInjected {\n controller: TryOnController\n theme: {\n getTheme(): ThemeSnapshot\n subscribe(listener: () => void): () => void\n setTheme(id: 'light' | 'dark'): void\n }\n /** Background occluder over the shared skin-background namespace. */\n background: SkinBackgroundHandle\n}\n\n/** Plugin-card component props: group-item runtime share + locale seat + injected face. */\nexport type SkinCenterComponentProps =\n PropsRuntime<'web-ui.plugin.item'> & PropsLocale<'skinCenter'> & SkinCenterInjected\n\n/** The apply target of the official stock-look card. */\nconst OFFICIAL = 'official'\n\n/** Skin ids that read the background-scrim variable and paint a backdrop. */\nconst BACKDROP_SKIN_IDS = new Set(['blue-fantasy', 'whale-song'])\n\n/**\n * Render the skin-center card: a disclosure header naming the plugin, with\n * the skin list (official default + every installed skin; try-on / theme\n * preview / one-click apply) inside its body.\n * @param props - card props.\n * @returns the plugin card.\n */\nexport function SkinCenter({ t, controller, theme, background }: SkinCenterComponentProps) {\n const snapshot = useSyncExternalStore(theme.subscribe, theme.getTheme)\n const opacity = useSyncExternalStore(background.subscribe, background.opacity)\n const activePackage = activeSkinEntry()?.package\n const activeId = activeSkinEntry()?.id\n const backdropActive = activeId !== undefined && BACKDROP_SKIN_IDS.has(activeId)\n const [open, setOpen] = useState(false)\n const [tryingId, setTryingId] = useState<string | null>(null)\n const [tryingOfficial, setTryingOfficial] = useState(false)\n const [applying, setApplying] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n\n const tryOn = (entry: SkinCenterEntry): void => {\n setError(null)\n void controller.tryOn(entry)\n .then(() => {\n setTryingId(entry.id)\n setTryingOfficial(false)\n })\n .catch(() => {\n // The controller may have torn down a previous session before the\n // load failed; reset both flags so no stale \"trying on\" lingers.\n setError(t('tryOnError'))\n setTryingId(null)\n setTryingOfficial(false)\n })\n }\n\n const tryOnOfficial = (): void => {\n setError(null)\n try {\n controller.tryOnOfficial()\n } catch {\n setError(t('tryOnError'))\n setTryingOfficial(false)\n return\n }\n setTryingId(null)\n setTryingOfficial(true)\n }\n\n const exitTryOn = (): void => {\n controller.exit()\n setTryingId(null)\n setTryingOfficial(false)\n }\n\n /**\n * Poll the host state until the config watcher reports the target active\n * (the patch write lands before the watcher re-applies it), or time out.\n * @param target - skin id, or `official` for the stock look.\n * @returns whether the target became active within the poll budget.\n */\n const confirmActive = (target: string): Promise<boolean> =>\n new Promise(resolve => {\n const expected = target === OFFICIAL ? 'none' : target\n let tries = 0\n const tick = (): void => {\n tries += 1\n void fetch('/api/skin-center/state')\n .then(async response => {\n const payload = await response.json().catch(() => null) as { ok?: boolean; active?: string } | null\n if (response.ok && payload?.ok === true && payload.active === expected) {\n resolve(true)\n return\n }\n if (tries >= 20) resolve(false)\n else window.setTimeout(tick, 250)\n })\n .catch(() => {\n if (tries >= 20) resolve(false)\n else window.setTimeout(tick, 250)\n })\n }\n tick()\n })\n\n /**\n * One-click apply: the host half runs `dsh-skin use <target>` (or\n * `use official`), the config watcher hot-reloads the patch within\n * seconds, then this page reloads to pick up the new boot graph.\n * @param target - skin id, or `official` for the stock look.\n */\n const applySkin = (target: string): void => {\n setError(null)\n setApplying(target)\n const body = target === OFFICIAL ? { official: true } : { skin: target }\n void fetch('/api/skin-center/apply', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n .then(async response => {\n const payload = await response.json().catch(() => null) as { ok?: boolean; error?: string } | null\n if (!response.ok || payload?.ok !== true) {\n throw new Error(payload?.error ?? `HTTP ${response.status}`)\n }\n setApplying(null)\n // Patch written; reload only once the watcher reports the target\n // active, so the page never boots into the old skin.\n void confirmActive(target).then(confirmed => {\n if (confirmed) {\n window.location.reload()\n } else {\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('appliedUnconfirmed')} — ${command}`)\n }\n })\n })\n .catch((cause: unknown) => {\n setApplying(null)\n const detail = cause instanceof Error ? cause.message : String(cause)\n const command = target === OFFICIAL ? 'dsh-skin use official' : `dsh-skin use ${target}`\n setError(`${t('applyFailed')} (${detail}) — ${command}`)\n })\n }\n\n const dark = snapshot.active.colorScheme === 'dark'\n\n /** One row: try-on control + apply button. Shared by the official card and every skin card. */\n const actionButtons = (opts: {\n key: string\n isActive: boolean\n isTrying: boolean\n onTryOn: () => void\n applyLabel: string\n }): ReactNode => (\n <div className={css.actions}>\n {opts.isActive ? (\n <button type=\"button\" className={`${css.button} ${css.buttonGhost}`} disabled>\n {t('tryOn')}\n </button>\n ) : opts.isTrying ? (\n <button type=\"button\" className={`${css.button} ${css.buttonPrimary}`} onClick={exitTryOn}>\n {t('exitTryOn')}\n </button>\n ) : (\n <button type=\"button\" className={`${css.button} ${css.buttonPrimary}`} onClick={opts.onTryOn}>\n {t('tryOn')}\n </button>\n )}\n <button\n type=\"button\"\n className={css.button}\n disabled={applying !== null}\n onClick={() => { applySkin(opts.key) }}\n >\n {applying === opts.key ? t('applying') : opts.applyLabel}\n </button>\n </div>\n )\n\n return (\n <li className={css.pluginCard}>\n <button\n type=\"button\"\n className={css.cardHeader}\n aria-expanded={open}\n aria-label={`${t(open ? 'collapse' : 'expand')}: ${t('title')}`}\n onClick={() => { setOpen(current => !current) }}\n >\n <span className={css.headText}>\n <span className={css.pluginName}>\n {t('title')}\n <span className={css.titleBadge}>{String(SKIN_CENTER_ENTRIES.length)}</span>\n </span>\n <span className={css.cardDescription} title={t('cardDescription')}>{t('cardDescription')}</span>\n </span>\n <span className={open ? css.chevronOpen : css.chevron}>▾</span>\n </button>\n\n {open\n ? (\n <div className={css.cardBody}>\n <div className={css.head}>\n <div className={css.intro} title={t('intro')}>{t('intro')}</div>\n <div className={css.themeRow}>\n <span className={css.themeLabel}>{t('theme')}</span>\n <button\n type=\"button\"\n className={`${css.themeButton} ${dark ? '' : css.themeButtonActive}`}\n onClick={() => { theme.setTheme('light') }}\n >\n {t('themeLight')}\n </button>\n <button\n type=\"button\"\n className={`${css.themeButton} ${dark ? css.themeButtonActive : ''}`}\n onClick={() => { theme.setTheme('dark') }}\n >\n {t('themeDark')}\n </button>\n </div>\n </div>\n\n <div className={css.backgroundRow}>\n <div className={css.backgroundHead}>\n <span className={css.backgroundLabel}>{t('backgroundOpacity')}</span>\n <span className={css.backgroundValue} aria-hidden=\"true\">{opacity}%</span>\n </div>\n <input\n id=\"skin-center-background-opacity\"\n className={css.backgroundRange}\n type=\"range\"\n min=\"0\"\n max=\"100\"\n step=\"5\"\n value={opacity}\n aria-valuetext={`${opacity}%`}\n aria-label={t('backgroundOpacity')}\n onChange={(event) => { background.set(Number(event.target.value)) }}\n />\n <p className={backdropActive ? css.backgroundHint : css.backgroundHintMuted}>\n {backdropActive ? t('backgroundHint') : t('backgroundHintInert')}\n </p>\n </div>\n\n {error !== null && <div className={css.error}>{error}</div>}\n\n <div className={css.list}>\n {(() => {\n const isActive = activePackage === undefined\n const isTrying = tryingOfficial\n const badge = isActive ? t('active') : isTrying ? t('tryingOn') : null\n return (\n <div className={css.card} key={OFFICIAL}>\n <div className={css.cardHead}>\n <span className={css.swatch} style={{ background: '#98a1ab' }} aria-hidden=\"true\" />\n <span className={css.cardName} title={t('official')}>{t('official')}</span>\n {badge !== null && (\n <span className={`${css.badge} ${isActive ? css.badgeActive : css.badgeTrying}`}>\n {badge}\n </span>\n )}\n </div>\n <div className={css.cardTagline} title={t('officialTagline')}>{t('officialTagline')}</div>\n {actionButtons({\n key: OFFICIAL,\n isActive,\n isTrying,\n onTryOn: tryOnOfficial,\n applyLabel: t('restore'),\n })}\n </div>\n )\n })()}\n\n {SKIN_CENTER_ENTRIES.map(entry => {\n const isActive = entry.package === activePackage\n const isTrying = entry.id === tryingId\n const badge = isActive ? t('active') : isTrying ? t('tryingOn') : null\n return (\n <div className={css.card} key={entry.id}>\n <div className={css.cardHead}>\n <span className={css.swatch} style={{ background: entry.accent }} aria-hidden=\"true\" />\n <span className={css.cardName} title={entry.nameEn}>{entry.nameEn}</span>\n {badge !== null && (\n <span className={`${css.badge} ${isActive ? css.badgeActive : css.badgeTrying}`}>\n {badge}\n </span>\n )}\n </div>\n <div className={css.cardTagline} title={entry.tagline}>{entry.tagline}</div>\n {actionButtons({\n key: entry.id,\n isActive,\n isTrying,\n onTryOn: () => { tryOn(entry) },\n applyLabel: t('apply'),\n })}\n </div>\n )\n })}\n </div>\n </div>\n )\n : null}\n </li>\n )\n}\n","/**\n * Background-scrim handle for the skin center: binds the `skin-background`\n * settings namespace and applies the chosen occlusion to the page's backdrop.\n *\n * Application is a CSS variable on `document.body`\n * (--dsw-skin-scrim), which backdrop-painting skins (blue-fantasy /\n * whale-song) read inside their setBackdrop() so the veil stays in sync across\n * theme flips and try-on restores. The official stock look paints no backdrop,\n * so the variable is inert there — the value still persists so it is ready for\n * the next backdrop skin.\n *\n * Values are 0-100 (0 = no extra veil, 100 = fully obscured); they are written\n * through as a 0..1 alpha for the CSS variable. Dragging the control applies\n * instantly (live) and persists through the settings scope.\n */\nimport type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-runtime/client'\n\n/** The namespace string the Host registers (mirrors src/index.ts). */\nexport const SKIN_BACKGROUND_NS = 'skin-background'\n\n/** Field of the background value inside the namespace section. */\nexport const OPACITY_FIELD = 'backgroundOpacity'\n\n/** CSS custom property written to document.body and read by backdrop skins. */\nexport const SCRIM_VAR = '--dsw-skin-scrim'\n\n/** Default occlusion (0 = no extra veil) when the section carries none. */\nexport const DEFAULT_OPACITY = 0\n\n/** The face the skin-center card injects for the background control. */\nexport interface SkinBackgroundHandle {\n /** Current occlusion 0-100 (also the getSnapshot seat for useSyncExternalStore). */\n opacity(): number\n /** Observe a change in the applied value. */\n subscribe(listener: () => void): () => void\n /** Apply + persist a new occlusion. */\n set(opacity: number): void\n}\n\n/**\n * Own the skin-background scope: read the latest occlusion, apply it to the\n * body CSS variable instantly, and persist changes through the settings scope.\n */\nexport class BackgroundController implements SkinBackgroundHandle {\n private value = DEFAULT_OPACITY\n private readonly listeners = new Set<() => void>()\n private readonly scope: SettingsScope<{ backgroundOpacity?: number }>\n\n /**\n * @param scope - the bound skin-background settings scope.\n */\n constructor(scope: SettingsScope<{ backgroundOpacity?: number }>) {\n this.scope = scope\n this.value = this.read()\n this.apply()\n scope.subscribe(() => {\n this.value = this.read()\n this.apply()\n this.publish()\n })\n }\n\n opacity(): number { return this.value }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n set(opacity: number): void {\n const clamped = Math.max(0, Math.min(100, Math.round(opacity)))\n this.value = clamped\n this.apply()\n this.publish()\n // Persist: queue the write on the settings scope. Failures are silent —\n // the live value is already applied, and the write drains on reconnect.\n void this.scope.set(OPACITY_FIELD, clamped)\n }\n\n /** The effective section value, clamped 0-100, defaulting to 0. */\n private read(): number {\n const snapshot: SettingsScopeSnapshot<{ backgroundOpacity?: number }> = this.scope.getSnapshot()\n const raw = snapshot.value?.backgroundOpacity\n if (typeof raw !== 'number' || !Number.isFinite(raw)) return DEFAULT_OPACITY\n return Math.max(0, Math.min(100, raw))\n }\n\n /** Write the current occlusion onto the body CSS variable (0..1 alpha). */\n private apply(): void {\n document.body.style.setProperty(SCRIM_VAR, String(this.value / 100))\n }\n\n private publish(): void {\n for (const listener of this.listeners) listener()\n }\n}\n","/**\n * Skin-center locale dictionaries. The plugin-card name, its description,\n * and every control of the in-GUI skin center is localized through the\n * standard `t` seat.\n */\n\n/** Copy keys owned by this plugin. */\nexport type SkinCenterKey =\n | 'title'\n | 'cardDescription'\n | 'expand'\n | 'collapse'\n | 'intro'\n | 'official'\n | 'officialTagline'\n | 'active'\n | 'tryingOn'\n | 'tryOn'\n | 'exitTryOn'\n | 'apply'\n | 'applying'\n | 'restore'\n | 'applyFailed'\n | 'appliedUnconfirmed'\n | 'theme'\n | 'themeLight'\n | 'themeDark'\n | 'tryOnError'\n | 'backgroundOpacity'\n | 'backgroundHint'\n | 'backgroundHintInert'\n\nexport const en: Record<SkinCenterKey, string> = {\n title: 'Skin Center',\n cardDescription: 'Try on any installed skin live in the GUI — exit restores instantly, applying persists in one click.',\n expand: 'Expand',\n collapse: 'Collapse',\n intro: 'Try on any skin live — it takes effect instantly, exit restores the current look. Apply persists it across restarts.',\n official: 'Official default',\n officialTagline: 'The stock DSH look with no skin applied.',\n active: 'Active',\n tryingOn: 'Trying on',\n tryOn: 'Try on',\n exitTryOn: 'Exit try-on',\n apply: 'Apply',\n applying: 'Applying…',\n restore: 'Restore',\n applyFailed: 'Apply failed',\n appliedUnconfirmed: 'Applied, but the change has not been confirmed — refresh the page if the skin did not switch',\n theme: 'Theme preview',\n themeLight: 'Light',\n themeDark: 'Dark',\n tryOnError: 'Try-on failed — see console',\n backgroundOpacity: 'Background occlusion',\n backgroundHint: 'Instantly veils the backdrop behind the panels — higher values obscure the art to help you focus.',\n backgroundHintInert: 'Only applies to skins that paint a backdrop (Blue Fantasy / Whale Song). Applies to the official default automatically once such a skin is active.',\n}\n\nexport const zh: Record<SkinCenterKey, string> = {\n title: '皮肤中心',\n cardDescription: '在 GUI 内即时试穿任意皮肤,退出即完全还原;应用一键完成并自动刷新。',\n expand: '展开',\n collapse: '收起',\n intro: '任意皮肤可即时试穿,退出即完全还原;「应用」一键持久化,页面自动刷新生效。',\n official: '官方默认',\n officialTagline: '还原 DSH 官方默认外观,不应用任何皮肤。',\n active: '当前激活',\n tryingOn: '试穿中',\n tryOn: '试穿',\n exitTryOn: '退出试穿',\n apply: '应用',\n applying: '应用中…',\n restore: '恢复默认',\n applyFailed: '应用失败',\n appliedUnconfirmed: '已写入配置但尚未确认生效——若皮肤未切换请手动刷新页面',\n theme: '主题预览',\n themeLight: '亮色',\n themeDark: '暗色',\n tryOnError: '试穿失败,详见控制台',\n backgroundOpacity: '背景遮挡',\n backgroundHint: '即时为面板背后的背景加遮罩——数值越高越能弱化插画,帮你集中注意力。',\n backgroundHintInert: '仅对带背景图插画的皮肤(蓝色幻想 / 鲸吟)生效;官方默认无背景图,该滑块对这些皮肤自动生效。',\n}\n","/**\n * In-GUI skin center, browser half: registers the Skins plugin card into the\n * Web UI plugin group (`web-ui.plugin.item`, declared by the web-ui-settings\n * group card under 插件配置) and provides the try-on controller + official\n * theme handle to it. The card lists every installed skin (embedded\n * registry), tries it on live inside the GUI, exits with a full restore, and\n * copies the one-command apply. The plugin writes only DOM and the settings\n * ledger — no services, no events, no model access.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ThemeRuntime } from '@deepseek-ai/dsh-client-ui-theme/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the settings-surface Context merge (ctx.settingsScope).\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { SkinCenter, type SkinCenterInjected } from './SkinCenter.tsx'\nimport { BackgroundController, SKIN_BACKGROUND_NS } from './background.ts'\nimport { en, zh, type SkinCenterKey } from './locales.ts'\nimport { TryOnController } from './try-on.ts'\n\nexport type { SkinCenterComponentProps, SkinCenterInjected } from './SkinCenter.tsx'\nexport { TryOnController } from './try-on.ts'\n\n/** Locale namespace owned by this plugin. */\nexport const NS = 'skinCenter'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The skin-center card's copy. */\n skinCenter: SkinCenterKey\n }\n\n interface SlotMap {\n /**\n * The child slot the Web UI plugin group declares; this card registers\n * into the group instead of the top-level `settings.plugin.item` list.\n * Spelled here with the same shape so this package can register without\n * depending on the sibling UI package.\n */\n 'web-ui.plugin.item': { kind: 'list'; scope: 'root'; owner: SettingsPluginItemOwnerProps }\n }\n}\n\n/** Owner share of a plugin card (the group card supplies nothing). */\nexport interface SettingsPluginItemOwnerProps {\n /** Marker field: card owner props are intentionally empty. */\n children?: never\n}\n\n/** Required services: slots + locale (plugin card), theme (preview toggle), and settingsScope + its transport (background scrim). */\nexport const inject = ['slots', 'locale', 'theme', 'settingsScope', 'connection', 'remote']\n\n/**\n * Register the skin-center dictionaries, the body scope attribute, and the\n * Skins plugin card inside the Web UI plugin group.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-skin-center: dictionaries')\n\n // The card's own styles scope under this attribute so they keep applying\n // during try-on (when the active skin's attribute is retracted).\n ctx.effect(() => {\n document.body.dataset.dshSkinCenter = ''\n return () => { delete document.body.dataset.dshSkinCenter }\n }, 'ui-skin-center: body scope')\n\n const theme = ctx.get('theme') as ThemeRuntime\n const controller = new TryOnController()\n // Background occluder over the shared skin-background namespace. The scope\n // is bound to this plugin's fiber, so it is torn down with the card.\n const backgroundScope = ctx.settingsScope.bind<{ backgroundOpacity?: number }>({ namespace: SKIN_BACKGROUND_NS })\n const background = new BackgroundController(backgroundScope)\n const injected = (): SkinCenterInjected => ({\n controller,\n theme: {\n getTheme: () => theme.getTheme(),\n subscribe: listener => ctx.on('theme/change', listener),\n setTheme: id => theme.setTheme(id),\n },\n background: {\n opacity: () => background.opacity(),\n subscribe: listener => background.subscribe(listener),\n set: opacity => background.set(opacity),\n },\n })\n\n ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({\n name: 'web-ui.plugin.item',\n id: 'skins',\n order: 110,\n locale: NS,\n inject: injected,\n }, SkinCenter))\n}\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -1,19 +1,300 @@
1
- import { execFile } from "node:child_process";
2
- import { readFileSync, readdirSync, statSync } from "node:fs";
3
- import { join } from "node:path";
1
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
2
+ import z from "schemastery";
3
+ import { lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
6
+ import { homedir } from "node:os";
7
+ //#region src/skin-switch.ts
8
+ /**
9
+ * In-process skin switching for the skin center — the official `dsh-skin use`
10
+ * CLI, re-implemented as a pure ESM module so the host half never needs a
11
+ * `dsh-skin` binary on PATH (the bug zhu1090093659/dsh-web-ui#5: "dsh-skin
12
+ * CLI not found on PATH").
13
+ *
14
+ * `use` owns the `dsh-skin managed` section of `~/.dsh/cordis.patch.yml`
15
+ * (atomic rewrite, hot-reloaded by the DSH config watcher within seconds,
16
+ * no restart) and the profile node_modules symlink that makes the selected
17
+ * skin resolvable from the web profile. `current` reads the active back.
18
+ *
19
+ * The behaviour/text is a 1:1 port of scripts/dsh-skin (`use`/`current`;
20
+ * workspace assets live in packages/skins/<id>). The skin registry is
21
+ * derived from each packages/skins/<id>/skin.json instead of a hand-written
22
+ * dictionary, so adding a skin needs no code change here.
23
+ * @module @linxin666/dsh-client-ui-skin-center/skin-switch
24
+ */
25
+ /** Repo layout: skin bundles live at packages/skins/<id>. */
26
+ const SKINS_DIR$1 = fileURLToPath(new URL("../../../skins/", import.meta.url));
27
+ /** Managed patch-section delimiters (the CLI's SINGLE authority boundaries). */
28
+ const MANAGED_START = "# --- dsh-skin managed (auto-generated; do not edit) ---";
29
+ const MANAGED_END = "# --- end dsh-skin managed ---";
30
+ /** The GUI profile this machine runs (dsh web); overridable via DSH_SKIN_PROFILE. */
31
+ const DEFAULT_PROFILE = process.env.DSH_SKIN_PROFILE ?? "web";
32
+ /**
33
+ * Parse the switch-relevant fields of one skin.json. Returns null for
34
+ * anything that is not a valid skin so it is simply skipped — never walking
35
+ * outside the skins tree (the id is validated before any path use).
36
+ * @param dir - directory name under packages/skins/.
37
+ */
38
+ function readSkinMeta(dir) {
39
+ try {
40
+ const meta = JSON.parse(readFileSync(join(SKINS_DIR$1, dir, "skin.json"), "utf8"));
41
+ if (typeof meta !== "object" || meta === null) return null;
42
+ const record = meta;
43
+ if (typeof record.id !== "string" || !/^[a-z0-9-]+$/.test(record.id)) return null;
44
+ if (typeof record.package !== "string") return null;
45
+ const wiring = record.wiring;
46
+ const wiringRecord = typeof wiring === "object" && wiring !== null ? wiring : null;
47
+ if (wiringRecord === null || typeof wiringRecord.id !== "string") return null;
48
+ return {
49
+ id: record.id,
50
+ package: record.package,
51
+ wiring: {
52
+ id: wiringRecord.id,
53
+ bundleWired: wiringRecord.bundleWired === true
54
+ }
55
+ };
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ /**
61
+ * Derive the skin registry from packages/skins/<id>/skin.json — the single
62
+ * source of truth (skin.json already carries package/wiring.id/bundleWired).
63
+ * Replaces the CLI's hand-maintained SKINS dictionary, so adding a skin
64
+ * needs no code change here.
65
+ * @returns skin id -> switch metadata.
66
+ */
67
+ function loadRegistry() {
68
+ const out = {};
69
+ for (const dir of readdirSync(SKINS_DIR$1)) {
70
+ const meta = readSkinMeta(dir);
71
+ if (meta === null || meta.wiring === void 0 || meta.package === void 0) continue;
72
+ out[meta.id] = {
73
+ pkg: meta.package,
74
+ id: meta.wiring.id,
75
+ dir: join(SKINS_DIR$1, dir),
76
+ bundleWired: meta.wiring.bundleWired === true
77
+ };
78
+ }
79
+ return out;
80
+ }
81
+ /**
82
+ * The skins the bundle layer already wires (no insert row needed) — derived
83
+ * from each skin.json wiring.bundleWired (the repo's static truth, e.g. xp).
84
+ *
85
+ * TODO: the CLI also detects skins wired via the active profile's
86
+ * dsh.profile.bundles (bundleWiredFromProfile). A skin installed from the
87
+ * web profile's manifest is still represented by skin.json's flag in this
88
+ * repo; wire further profile-based detection here if ever needed.
89
+ * @param registry - the derived registry (or a partial override in tests).
90
+ */
91
+ function wiredNames(registry) {
92
+ const out = /* @__PURE__ */ new Set();
93
+ for (const [name, skin] of Object.entries(registry)) if (skin.bundleWired) out.add(name);
94
+ return out;
95
+ }
96
+ /**
97
+ * Drop legacy hand-written skin rows (insert rows with a name) and old touch
98
+ * comments. The CLI regex matched the historical @deepseek-ai scope; this
99
+ * also matches the current @linxin666 scope so stale rows are always cleaned.
100
+ * @param patch - raw patch file text.
101
+ */
102
+ function stripLegacySkinRows(patch) {
103
+ return patch.replace(/^ # [^\n]*\n - id: ui-skin-[^\n]+\n name: '@(?:deepseek-ai|linxin666)\/dsh-client-ui-skin-[^\n]+'\n/gm, "").replace(/^# \(touch\)[^\n]*\n?/gm, "").replace(/\n{3,}/g, "\n\n");
104
+ }
105
+ /**
106
+ * Remove the managed skin section. Throws on an unterminated section (a
107
+ * malformed boot patch must fail loudly, never be silently half-written).
108
+ * @param patch - raw patch file text.
109
+ */
110
+ function stripManaged(patch) {
111
+ const start = patch.indexOf(MANAGED_START);
112
+ if (start === -1) return patch;
113
+ const end = patch.indexOf(MANAGED_END, start);
114
+ if (end === -1) throw new Error("managed skin section is unterminated; fix ~/.dsh/cordis.patch.yml");
115
+ return patch.slice(0, start) + patch.slice(end + 30);
116
+ }
117
+ /**
118
+ * Render the managed section for a target skin (null = official stock look:
119
+ * every skin disabled, no insert row). A wired active skin also needs no
120
+ * insert row — the bundle layer already provides it.
121
+ * @param active - skin id, or null for the official stock look.
122
+ * @param registry - registry to render against (defaults to the repo registry).
123
+ */
124
+ function renderManaged(active, registry = loadRegistry()) {
125
+ const wired = wiredNames(registry);
126
+ const lines = [MANAGED_START];
127
+ for (const name of Object.keys(registry)) {
128
+ if (name === active) continue;
129
+ lines.push(`- id: ${registry[name].id}`, " disabled: true");
130
+ }
131
+ if (active !== null && !wired.has(active)) lines.push("- insert:", ` - id: ${registry[active].id}`, ` name: '${registry[active].pkg}'`);
132
+ lines.push(MANAGED_END);
133
+ return lines.join("\n");
134
+ }
135
+ /**
136
+ * Which skin is currently enabled, read from a patch file. With bundle-wired
137
+ * skins the active skin carries no insert row, so the answer is the
138
+ * bundle-wired skin that the patch does NOT disable; the legacy reading
139
+ * (last non-disabled skin row) remains for pre-bundle layouts.
140
+ * @param patch - raw patch file text.
141
+ * @param registry - registry to read against (defaults to the repo registry).
142
+ */
143
+ function currentActive(patch, registry = loadRegistry()) {
144
+ const disabled = /* @__PURE__ */ new Set();
145
+ for (const m of patch.matchAll(/^- id: (ui-skin-[a-z0-9-]+)\n disabled: true/gm)) disabled.add(m[1]);
146
+ const wired = wiredNames(registry);
147
+ for (const [name, skin] of Object.entries(registry)) if (wired.has(name) && !disabled.has(skin.id)) return name;
148
+ const rows = [...patch.matchAll(/(?:^|\n) *- id: (ui-skin-[a-z0-9-]+)(\n *disabled: (true))?/g)];
149
+ const enabled = [];
150
+ for (const m of rows) if (!m[3]) enabled.push(m[1]);
151
+ return enabled.length ? enabled[enabled.length - 1].replace("ui-skin-", "") : null;
152
+ }
153
+ /**
154
+ * Resolve the DSH paths under a HOME. home/profile are injectable so tests
155
+ * can point at a throwaway HOME (mirrors scripts/dsh-skin.test.mjs).
156
+ * @param home - home dir (defaults to the process HOME).
157
+ * @param profile - profile name (defaults to DSH_SKIN_PROFILE or 'web').
158
+ */
159
+ function resolvePaths(home = homedir(), profile = DEFAULT_PROFILE) {
160
+ return {
161
+ patchPath: join(home, ".dsh", "cordis.patch.yml"),
162
+ profileModulesDir: join(home, ".dsh", "profiles", profile, "node_modules")
163
+ };
164
+ }
165
+ function readPatch(patchPath) {
166
+ try {
167
+ return readFileSync(patchPath, "utf8");
168
+ } catch {
169
+ return "";
170
+ }
171
+ }
172
+ /**
173
+ * Atomic replace: write a sibling temp file then rename over the target, so a
174
+ * crash mid-write can never leave a half-written boot patch and the config
175
+ * watcher only ever sees complete content (the CLI's own strategy). Creates
176
+ * the parent dir if missing.
177
+ * @param filePath - target file.
178
+ * @param next - full next content.
179
+ */
180
+ function writePatchAtomic(filePath, next) {
181
+ mkdirSync(dirname(filePath), { recursive: true });
182
+ const tmp = `${filePath}.tmp-${process.pid}`;
183
+ writeFileSync(tmp, next);
184
+ renameSync(tmp, filePath);
185
+ }
186
+ /**
187
+ * Make the profile node_modules symlink for a skin. Returns true when a new
188
+ * link was created, false when it already pointed at the skin dir. Refuses
189
+ * to touch a non-symlink target (that path is not ours to clobber).
190
+ * @param entry - the skin switch entry.
191
+ * @param profileModulesDir - the profile's node_modules dir.
192
+ */
193
+ function ensureSymlink(entry, profileModulesDir) {
194
+ const target = join(profileModulesDir, entry.pkg);
195
+ let stat = null;
196
+ try {
197
+ stat = lstatSync(target);
198
+ } catch {}
199
+ if (stat) {
200
+ if (!stat.isSymbolicLink()) throw new Error(`${target} exists and is not a symlink — refusing to touch it`);
201
+ if (readlinkSync(target) === entry.dir) return false;
202
+ unlinkSync(target);
203
+ }
204
+ mkdirSync(dirname(target), { recursive: true });
205
+ symlinkSync(entry.dir, target);
206
+ return true;
207
+ }
208
+ /** Windows/privilege code points where symlinkSync fails. */
209
+ const SYMLINK_PRIVILEGE_CODES = [
210
+ "EPERM",
211
+ "EACCES",
212
+ "ENOSYS"
213
+ ];
214
+ /**
215
+ * Wrap a symlink-labelled failure (typ. Windows without developer mode or
216
+ * elevated privileges) in a human-readable hint instead of a bare fs error.
217
+ * @param caller - the operation label for the error message.
218
+ * @param fn - the fs call to run.
219
+ */
220
+ function symlinkFriendly(caller, fn) {
221
+ try {
222
+ return fn();
223
+ } catch (error) {
224
+ const code = error?.code;
225
+ if (typeof code === "string" && SYMLINK_PRIVILEGE_CODES.includes(code)) throw new Error(`${caller} 需要为皮肤创建符号链接,但权限不足(${code})。Windows 请以管理员身份或开启开发者模式后重试;若已手动把皮肤装进 profile,可跳过本步。`);
226
+ throw error;
227
+ }
228
+ }
229
+ /**
230
+ * Optional soft warning when a skin is not resolvable from the web profile
231
+ * (the CLI prints it; surfaced as a soft hint, not a failure — the patch
232
+ * stays authoritative).
233
+ * @param entry - the skin switch entry.
234
+ * @param profileModulesDir - the profile's node_modules dir.
235
+ */
236
+ function checkInstalled(entry, profileModulesDir) {
237
+ let ok = false;
238
+ try {
239
+ ok = lstatSync(join(profileModulesDir, entry.pkg)).isSymbolicLink();
240
+ } catch {}
241
+ return ok ? null : `${entry.pkg} 未安装到 profile;先用 dsh-skin install ${entry.id.replace(/^ui-skin-/, "")}(或 dsh plugin --profile ${DEFAULT_PROFILE} add ${entry.dir})安装,否则加载会失败。`;
242
+ }
243
+ /**
244
+ * Switch the active skin. Equivalent to `dsh-skin use <name>`:
245
+ * 1. makes the profile node_modules symlink for a non-official skin,
246
+ * 2. rewrites the managed section of the boot patch atomically.
247
+ * Returns the same stdout the CLI would print (drives the GUI message).
248
+ * @param name - skin id, or 'official' for the stock look.
249
+ * @param opts - injectable HOME/profile/registry (tests use a throwaway HOME).
250
+ * @returns the human-facing confirmation string.
251
+ */
252
+ function useSkin(name, opts = {}) {
253
+ const official = name === "official";
254
+ const registry = opts.registry ?? loadRegistry();
255
+ if (!official && registry[name] === void 0) throw new Error(`unknown skin "${name}". Known: ${Object.keys(registry).join(", ")} (or "official" for the stock look)`);
256
+ const paths = resolvePaths(opts.home, opts.profile);
257
+ const notices = [];
258
+ if (!official) {
259
+ const entry = registry[name];
260
+ symlinkFriendly(`switching to "${name}"`, () => {
261
+ ensureSymlink(entry, paths.profileModulesDir);
262
+ });
263
+ const warn = checkInstalled(entry, paths.profileModulesDir);
264
+ if (warn !== null) notices.push(warn);
265
+ }
266
+ const next = `${stripLegacySkinRows(stripManaged(readPatch(paths.patchPath))).replace(/\s+$/, "")}\n\n${renderManaged(official ? null : name, registry)}\n`;
267
+ writePatchAtomic(paths.patchPath, next);
268
+ const core = official ? "restored the official stock look — the config watcher applies it within seconds; refresh the page to see it." : `skin switched to "${name}" — the config watcher applies it within seconds; refresh the page (or the manifest re-fetches) to see it.`;
269
+ return notices.length ? `${core}\n${notices.join("\n")}` : core;
270
+ }
271
+ /**
272
+ * Read the active skin, mirroring `dsh-skin current` (prints the name or
273
+ * 'none'). The patch is read from disk by default; a caller can pass the text
274
+ * it already holds.
275
+ * @param patch - optional pre-read patch text.
276
+ * @param opts - injectable HOME/profile/registry.
277
+ * @returns the active skin id, or 'none' for the stock look.
278
+ */
279
+ function currentSkin(patch, opts = {}) {
280
+ const paths = resolvePaths(opts.home, opts.profile);
281
+ const registry = opts.registry ?? loadRegistry();
282
+ return currentActive(patch ?? readPatch(paths.patchPath), registry) ?? "none";
283
+ }
284
+ //#endregion
5
285
  //#region src/routes.ts
6
286
  /**
7
287
  * Skin-center HTTP routes — the browser half talks to the host through plain
8
288
  * same-origin endpoints: JSON for state/apply, plus the bundle route serving
9
289
  * each skin's prebuilt `lib/client.js` as a same-origin script for live
10
290
  * try-on (the GUI never embeds the ~700KB of art base64 in its own bundle).
11
- * The host half delegates skin switching to the `dsh-skin` CLI
12
- * (the single authority over the `dsh-skin managed` section of
13
- * `~/.dsh/cordis.patch.yml` and the profile symlink), so switching skins from
14
- * the GUI is exactly `dsh-skin use <name>` — the config watcher hot-reloads
15
- * the patch within seconds and the frontend reloads the page to pick up the
16
- * new boot graph. Same pattern as dsh-pet's `/api/pet` family.
291
+ * The host half switches skins in-process (src/skin-switch.ts) an ESM port
292
+ * of the `dsh-skin` CLI that owns the `dsh-skin managed` section of
293
+ * `~/.dsh/cordis.patch.yml` and the profile symlink, exactly like
294
+ * `dsh-skin use <name>` — so no `dsh-skin` binary is required on PATH
295
+ * (the bug zhu1090093659/dsh-web-ui#5). The config watcher hot-reloads the
296
+ * patch within seconds and the frontend reloads the page to pick up the new
297
+ * boot graph. Same pattern as dsh-pet's `/api/pet` family.
17
298
  *
18
299
  * Unlike pet's behavioral endpoints, `/apply` writes the user's boot config,
19
300
  * so every route also rejects cross-site requests (Sec-Fetch-Site / Origin
@@ -23,8 +304,6 @@ import { fileURLToPath } from "node:url";
23
304
  */
24
305
  /** Browser-facing base path of the skin-center API. */
25
306
  const SKIN_CENTER_API_PREFIX = "/api/skin-center";
26
- /** Cap a dsh-skin invocation; a hung CLI must never block the server. */
27
- const DSH_SKIN_TIMEOUT_MS = 15e3;
28
307
  /** One JSON response. */
29
308
  function json(res, status, body) {
30
309
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
@@ -100,27 +379,20 @@ function readJsonBody(req) {
100
379
  });
101
380
  }
102
381
  /**
103
- * Run `dsh-skin <args>` and resolve with its stdout.
104
- * @param args - CLI arguments (e.g. `['use', 'qq98']`).
105
- * @returns stdout on exit code 0.
106
- * @throws the CLI's stderr (or the spawn error) on any failure.
382
+ * In-process runner fulfilling the `dsh-skin <args>` contract used by the
383
+ * routes (`['use', <name>]` and `['current']`). It never spawns a PATH
384
+ * binary it calls the embedded port of the CLI (src/skin-switch.ts), which
385
+ * writes the boot patch and the profile symlink directly. Returns the same
386
+ * stdout text the CLI would print, and rejects with the same error messages.
387
+ * @param args - command arguments (e.g. `['use', 'qq98']`).
107
388
  */
108
389
  function runDshSkin(args) {
109
- return new Promise((resolve, reject) => {
110
- execFile("dsh-skin", args, { timeout: DSH_SKIN_TIMEOUT_MS }, (error, stdout, stderr) => {
111
- if (error === null) {
112
- resolve(stdout);
113
- return;
114
- }
115
- const spawnError = error;
116
- if (spawnError.code === "ENOENT") {
117
- reject(/* @__PURE__ */ new Error("dsh-skin CLI not found on PATH — install it from dsh-web-ui/scripts/dsh-skin"));
118
- return;
119
- }
120
- const detail = (stderr ?? "").trim() || spawnError.message;
121
- reject(new Error(detail || `dsh-skin ${args.join(" ")} failed`));
122
- });
123
- });
390
+ const [command, argument] = args;
391
+ switch (command) {
392
+ case "use": return Promise.resolve(useSkin(argument));
393
+ case "current": return Promise.resolve(currentSkin(void 0));
394
+ default: return Promise.reject(/* @__PURE__ */ new Error(`unexpected dsh-skin command: ${args.join(" ")}`));
395
+ }
124
396
  }
125
397
  /** A GET route wrapping one async call, fenced to same-origin requests. */
126
398
  function getRoute(path, run) {
@@ -260,11 +532,11 @@ function makeSkinCenterRoutes(deps = {}) {
260
532
  })),
261
533
  bundleRoute(),
262
534
  postRoute(`${SKIN_CENTER_API_PREFIX}/apply`, async (body) => {
263
- const skin = body.skin;
264
535
  const official = body.official === true;
265
- if (typeof skin !== "string" || skin === "") {
266
- if (!official) throw new Error("invalid-skin: pass a skin name or official: true");
267
- } else if (official) throw new Error("invalid-skin: skin and official are mutually exclusive");
536
+ const skin = body.skin;
537
+ if (official) {
538
+ if (skin !== void 0) throw new Error("invalid-skin: skin and official are mutually exclusive");
539
+ } else if (typeof skin !== "string" || skin === "") throw new Error("invalid-skin: pass a skin name or official: true");
268
540
  const out = await run(["use", official ? "official" : skin]);
269
541
  return {
270
542
  ok: true,
@@ -281,6 +553,14 @@ const name = "ui-skin-center";
281
553
  /** Services required before the skin-center can mount its routes. */
282
554
  const inject = ["webServer"];
283
555
  /**
556
+ * Settings namespace for the main-interface background scrim, owned by the
557
+ * skin center. The browser half spells the same string so it can bind the
558
+ * scope without depending on this Host package.
559
+ */
560
+ const SKIN_BACKGROUND_NAMESPACE = settingsNamespace("skin-background");
561
+ /** Runtime schema for SkinBackgroundConfig. */
562
+ const SkinBackgroundConfigSchema = z.object({ backgroundOpacity: z.number().min(0).max(100).step(5).default(0) });
563
+ /**
284
564
  * Register the skin-center API routes.
285
565
  *
286
566
  * Failure policy: route mounting problems are logged, never thrown — the web
@@ -289,6 +569,10 @@ const inject = ["webServer"];
289
569
  * @param ctx - cordis context.
290
570
  */
291
571
  function apply(ctx) {
572
+ installSettingsSection(ctx, SKIN_BACKGROUND_NAMESPACE, SkinBackgroundConfigSchema, {}, {
573
+ setSource: () => {},
574
+ onChange: () => {}
575
+ });
292
576
  const routes = makeSkinCenterRoutes();
293
577
  try {
294
578
  ctx.effect(() => {
@@ -308,4 +592,4 @@ function apply(ctx) {
308
592
  }
309
593
  }
310
594
  //#endregion
311
- export { SKIN_CENTER_API_PREFIX, apply, inject, makeSkinCenterRoutes, name };
595
+ export { SKIN_BACKGROUND_NAMESPACE, SKIN_CENTER_API_PREFIX, SkinBackgroundConfigSchema, apply, inject, makeSkinCenterRoutes, name };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-skin-center",
3
3
  "description": "In-GUI skin center for the dsh web GUI: lists the official default plus every installed skin (qq98 / ths / xp / blue-fantasy / dragon-heir / minecraft), tries it on live inside the real GUI (executes the actual client bundle, light/dark preview), exits with a full restore, and applies in one click through the host /api/skin-center API (dsh-skin use, hot-reloaded, no restart)",
4
- "version": "0.1.1",
4
+ "version": "0.1.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -36,15 +36,22 @@
36
36
  "@deepseek-ai/cordis": "^4.0.1",
37
37
  "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
38
38
  "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
39
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
39
40
  "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
40
41
  "@deepseek-ai/dsh-client-ui-theme": "^0.1.0-rc.6",
41
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6"
42
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
43
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
44
+ "schemastery": "^3.18.0"
42
45
  },
43
46
  "files": [
44
47
  "lib",
45
48
  "cordis.patch.yml",
46
49
  "README.md"
47
50
  ],
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/zhu1090093659/dsh-web-ui.git"
54
+ },
48
55
  "scripts": {
49
56
  "build": "tsdown",
50
57
  "watch": "tsdown --watch",