dsh-background-by-model 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -33,6 +33,7 @@ window.__ModuleLoader__.load({
33
33
  uiOpacityBg: "主背景",
34
34
  uiOpacitySide: "左侧栏",
35
35
  uiOpacityCard: "卡片与菜单面板",
36
+ uiOpacityCode: "代码块",
36
37
  uiOpacityInput: "输入框与控件",
37
38
  uiSop: "设置界面透明度",
38
39
  uiChatRegion: "对话文本框",
@@ -83,6 +84,7 @@ window.__ModuleLoader__.load({
83
84
  ruleColorTargetImage: "作用于选中的图片:",
84
85
  ruleColorTargetRule: "作用于本规则(它还没有图片)",
85
86
  ruleLayout: "布局模式",
87
+ ruleLayoutNoImage: "添加图片后可分别设置每张图的布局模式",
86
88
  ruleFramingEdit: "编辑位置",
87
89
  ruleOpacity: "背景透明度",
88
90
  ruleBlur: "背景模糊",
@@ -141,6 +143,7 @@ window.__ModuleLoader__.load({
141
143
  importDone: "已导入配置",
142
144
  importFail: "导入失败,文件格式不正确",
143
145
  footerTag: "按模型换背景插件",
146
+ repoLink: "在 GitHub 上打开仓库",
144
147
  holidayTitle: "节日特殊背景",
145
148
  holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)",
146
149
  trTitle: "切换效果",
@@ -174,6 +177,7 @@ window.__ModuleLoader__.load({
174
177
  uiOpacityBg: "Main background",
175
178
  uiOpacitySide: "Left panel",
176
179
  uiOpacityCard: "Cards & menus",
180
+ uiOpacityCode: "Code blocks",
177
181
  uiOpacityInput: "Input & controls",
178
182
  uiSop: "Settings interface opacity",
179
183
  uiChatRegion: "Conversation text frame",
@@ -224,6 +228,7 @@ window.__ModuleLoader__.load({
224
228
  ruleColorTargetRule: "Applies to this rule (it has no image yet)",
225
229
  ruleColorExtract: "Extract from image",
226
230
  ruleLayout: "Layout mode",
231
+ ruleLayoutNoImage: "Add an image to give each one its own layout mode",
227
232
  ruleFramingEdit: "Edit position",
228
233
  ruleOpacity: "Background opacity",
229
234
  ruleBlur: "Background blur",
@@ -282,6 +287,7 @@ window.__ModuleLoader__.load({
282
287
  importDone: "Profile imported",
283
288
  importFail: "Import failed — invalid file",
284
289
  footerTag: "Per-model wallpaper plugin",
290
+ repoLink: "Open the repository on GitHub",
285
291
  holidayTitle: "Holiday backgrounds",
286
292
  holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)",
287
293
  trTitle: "Switch effect",
@@ -578,18 +584,22 @@ window.__ModuleLoader__.load({
578
584
  * `sidebar` sidebar (`--dsw-specific-sidebar-fill`)
579
585
  * `card` cards/panels (`--dsw-alias-bg-layer-1/2/3`, `--dsw-specific-menu`
580
586
  * and its 0.1.7 alias `--dsw-menu-surface-fill`)
587
+ * `code` code blocks, banner included (`--dsw-alias-markdown-code-block`,
588
+ * `--dsw-alias-markdown-code-block-banner`)
581
589
  * `input` input/control surfaces (`--dsw-specific-input-major`)
582
590
  */
583
591
  const PART_OPACITY_KEYS = [
584
592
  "bg",
585
593
  "sidebar",
586
594
  "card",
595
+ "code",
587
596
  "input"
588
597
  ];
589
598
  const DEFAULT_PART_OPACITIES = {
590
599
  bg: .85,
591
600
  sidebar: .93,
592
601
  card: 1,
602
+ code: 1,
593
603
  input: 1
594
604
  };
595
605
  /**
@@ -722,7 +732,9 @@ window.__ModuleLoader__.load({
722
732
  *
723
733
  * Holidays default to `fill` rather than a rule's `fit`: these are full-bleed
724
734
  * festival wallpapers, and `fit` would letterbox them on every aspect ratio the
725
- * art was not cut for.
735
+ * art was not cut for. The mode now lives on the IMAGE (see `BgImage.bgMode`), so
736
+ * that default is written there — and `fill` is what a holiday's read-back
737
+ * sanitizer refuses to lose (see `normalizeHolidayRule`).
726
738
  */
727
739
  function defaultHolidayRule(def) {
728
740
  const color = hexToHsl(def.color);
@@ -730,13 +742,13 @@ window.__ModuleLoader__.load({
730
742
  id: def.id,
731
743
  images: [{
732
744
  slot: def.slot,
745
+ bgMode: "fill",
733
746
  bgState: { ...DEFAULT_BG_STATE },
734
747
  color
735
748
  }],
736
749
  match: "",
737
750
  enabled: true,
738
751
  color,
739
- bgMode: "fill",
740
752
  wallpaperOpacity: 1,
741
753
  blur: 0,
742
754
  rotate: defaultRotation()
@@ -867,12 +879,21 @@ window.__ModuleLoader__.load({
867
879
  * same as absent: an entry with `color: null` is an image the user explicitly set
868
880
  * to "follow the system theme", and re-inheriting the rule's color over that
869
881
  * would undo a deliberate choice on every load.
882
+ *
883
+ * `inheritMode` is the same one-shot lift for the layout mode, and it is the
884
+ * caller's to resolve because the two callers answer differently: a user rule
885
+ * passes the mode its config carried at the RULE level (a schema-5 config, where
886
+ * 适应/填充 was one choice for the whole rotation), a holiday passes `fill` (its
887
+ * own default, which is not up for the lift to change). An entry that HAS a
888
+ * `bgMode` key keeps it — including one that has just been set to a value equal
889
+ * to the default, which is a decision and not an absence.
870
890
  */
871
- function normalizeImage(raw, inherit = null) {
891
+ function normalizeImage(raw, inherit = null, inheritMode = "fit") {
872
892
  const i = raw ?? {};
873
893
  if (typeof i.slot !== "string" || !SLOT_RE.test(i.slot)) return null;
874
894
  return {
875
895
  slot: i.slot,
896
+ bgMode: BG_MODES$1.includes(i.bgMode) ? i.bgMode : inheritMode,
876
897
  bgState: normalizeBgState(i.bgState),
877
898
  color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit
878
899
  };
@@ -892,6 +913,14 @@ window.__ModuleLoader__.load({
892
913
  * it as well: it is not a default for its images at paint time any more, but it
893
914
  * is still what the rule paints once its last picture is gone.
894
915
  *
916
+ * The layout mode makes the same trip in the other direction, and this is the
917
+ * last release that has to: a config written before 0.8 keeps `bgMode` at the RULE
918
+ * level, where one value covered every picture of a rotation, so it is read here
919
+ * once and handed to each image as its inherited mode. What the user chose
920
+ * therefore survives the move the same way it survived the color's — the mode is
921
+ * not reset to the default on upgrade, and the field it used to live on is simply
922
+ * not written back.
923
+ *
895
924
  * An EMPTY list is a legitimate rule, not a broken one: removing a rule's last
896
925
  * picture has to be expressible, and forcing an entry back in is what produced a
897
926
  * phantom blank image that took slot 1 and could never be deleted again (the next
@@ -906,11 +935,12 @@ window.__ModuleLoader__.load({
906
935
  const id = typeof r.id === "string" && r.id !== "" ? r.id : null;
907
936
  if (id === null) return null;
908
937
  const color = normalizeHsl(r.color);
938
+ const mode = BG_MODES$1.includes(r.bgMode) ? r.bgMode : "fit";
909
939
  const images = [];
910
940
  const seen = /* @__PURE__ */ new Set();
911
941
  const hadList = Array.isArray(r.images);
912
942
  if (hadList) for (const entry of r.images) {
913
- const image = normalizeImage(entry, color);
943
+ const image = normalizeImage(entry, color, mode);
914
944
  if (image === null || seen.has(image.slot)) continue;
915
945
  seen.add(image.slot);
916
946
  images.push(image);
@@ -918,21 +948,22 @@ window.__ModuleLoader__.load({
918
948
  const legacySlot = typeof r.slot === "string" && SLOT_RE.test(r.slot) ? r.slot : null;
919
949
  if (images.length === 0 && legacySlot !== null) images.push({
920
950
  slot: legacySlot,
951
+ bgMode: mode,
921
952
  bgState: normalizeBgState(r.bgState),
922
953
  color
923
954
  });
924
955
  if (images.length === 0 && !hadList) return null;
925
- const mode = BG_MODES$1.includes(r.bgMode) ? r.bgMode : "fit";
956
+ const rotate = normalizeRotation(r.rotate);
957
+ if (images.length < 2) rotate.enabled = false;
926
958
  return {
927
959
  id,
928
960
  images,
929
961
  match: typeof r.match === "string" ? r.match : "",
930
962
  enabled: r.enabled !== false,
931
963
  color,
932
- bgMode: mode,
933
964
  wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
934
965
  blur: clamp$1(r.blur, 0, 60, 0),
935
- rotate: normalizeRotation(r.rotate)
966
+ rotate
936
967
  };
937
968
  }
938
969
  /**
@@ -943,9 +974,12 @@ window.__ModuleLoader__.load({
943
974
  * its bytes, and its theme color is a fixed part of what that holiday looks like —
944
975
  * so a stale or hand-edited value would either orphan the wallpaper, point the
945
976
  * holiday at a rule's image, or paint a festival in a color it does not have.
946
- * Everything else is sanitized exactly like a rule's field — except the image
947
- * list and the rotation, which stay the definition's own single image and stay
948
- * off, so a festival can never be swapped or cycled.
977
+ * Everything else is sanitized exactly like a rule's field — except three things
978
+ * that stay the definition's own: the image list and the rotation (so a festival
979
+ * can never be swapped or cycled), and the layout mode, which is the definition's
980
+ * `fill` for the same reason the color is its own — full-bleed art letterboxed by
981
+ * a stale `fit` is the one thing this feature must not do, and the mode is not
982
+ * editable for a holiday anywhere in the panel.
949
983
  */
950
984
  function normalizeHolidayRule(def, raw) {
951
985
  const r = raw ?? {};
@@ -953,11 +987,11 @@ window.__ModuleLoader__.load({
953
987
  return {
954
988
  ...base,
955
989
  enabled: r.enabled !== false,
956
- bgMode: BG_MODES$1.includes(r.bgMode) ? r.bgMode : base.bgMode,
957
990
  wallpaperOpacity: clamp01(r.wallpaperOpacity, base.wallpaperOpacity),
958
991
  blur: clamp$1(r.blur, 0, 60, base.blur),
959
992
  images: base.images.map((image) => ({
960
993
  slot: image.slot,
994
+ bgMode: image.bgMode,
961
995
  bgState: normalizeBgState(r.images?.[0]?.bgState ?? r.bgState),
962
996
  color: image.color
963
997
  }))
@@ -1194,7 +1228,6 @@ window.__ModuleLoader__.load({
1194
1228
  match: "",
1195
1229
  enabled: true,
1196
1230
  color: null,
1197
- bgMode: "fit",
1198
1231
  wallpaperOpacity: 1,
1199
1232
  blur: 0,
1200
1233
  rotate: defaultRotation()
@@ -1255,8 +1288,17 @@ window.__ModuleLoader__.load({
1255
1288
  .25
1256
1289
  ];
1257
1290
  }
1291
+ /**
1292
+ * Layout mode of the CURRENT image (per image, see BgImage).
1293
+ *
1294
+ * Read from the image and never from the rule, which no longer has a mode at all:
1295
+ * a rotation through a photo and a screenshot has to be able to letterbox one and
1296
+ * fill the other, and a rule-level answer would be the wrong one for half the
1297
+ * list. A rule with no image has nothing to place, so it answers with the
1298
+ * default — the renderer's null case, painted as "no wallpaper".
1299
+ */
1258
1300
  function rBgMode() {
1259
- return activeRule()?.bgMode ?? "fit";
1301
+ return activeImage()?.bgMode ?? "fit";
1260
1302
  }
1261
1303
  function rWop() {
1262
1304
  return clamp01(activeRule()?.wallpaperOpacity, 1);
@@ -1510,7 +1552,7 @@ window.__ModuleLoader__.load({
1510
1552
  const d = data;
1511
1553
  const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
1512
1554
  const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
1513
- hostShape = schema === null ? "legacy" : schema >= 5 ? "current" : "legacy";
1555
+ hostShape = schema === null ? "legacy" : schema >= 6 ? "current" : "legacy";
1514
1556
  if (hostShape === "legacy") holdWrite();
1515
1557
  return {
1516
1558
  config: d.config,
@@ -1602,9 +1644,13 @@ window.__ModuleLoader__.load({
1602
1644
  const names = Object.keys(input.initial).filter((n) => n.startsWith("--"));
1603
1645
  if (names.length === 0) return "";
1604
1646
  const css = registrations(names, input.initial);
1605
- if (!(input.durationMs > 0) || input.selectors.length === 0) return css;
1606
- const list = names.map((n) => `${n} ${input.durationMs}ms ${input.easing}`).join(",");
1607
- return `${css}${input.selectors.join(",")}{transition:${list}}`;
1647
+ if (!(input.durationMs > 0)) return css;
1648
+ const written = new Set(input.written);
1649
+ const timing = (n) => `${n} ${input.durationMs}ms ${input.easing}`;
1650
+ return `${css}${input.scopes.map((s) => ({
1651
+ selector: s.selector,
1652
+ transitions: [...s.tokens.filter((n) => names.includes(n) && written.has(n)), ...s.properties ?? []]
1653
+ })).filter((s) => s.transitions.length > 0).map((s) => `${s.selector}{transition:${s.transitions.map(timing).join(",")}}`).join("")}`;
1608
1654
  }
1609
1655
  /**
1610
1656
  * The same fade for an element the plugin paints INLINE (its columns and cards).
@@ -1774,6 +1820,7 @@ window.__ModuleLoader__.load({
1774
1820
  "--dsw-alias-interactive-bg-hover": rgba(h(0), s(0), l(-.3), .08),
1775
1821
  "--dsw-alias-interactive-bg-active": rgba(h(0), s(0), l(-.3), .14),
1776
1822
  "--dsw-alias-markdown-code-block": hsl(h(0), s(-.1), l(-.03)),
1823
+ "--dsw-alias-markdown-code-block-banner": hsl(h(0), s(-.1), l(-.06)),
1777
1824
  "--dsw-alias-markdown-inline-code": hsl(h(0), s(-.08), l(.04)),
1778
1825
  "--dsw-specific-sidebar-fill": hsl(h(0), s(-.1), l(-.03)),
1779
1826
  "--dsw-specific-sidebar-nav-item-active": hsl(h(0), s(-.08), l(.05)),
@@ -1999,7 +2046,11 @@ window.__ModuleLoader__.load({
1999
2046
  /* ── shell: nav rail + page body ─────────────────────────────────────────── */
2000
2047
  .dab-shell{display:grid;grid-template-columns:158px minmax(0,1fr);gap:26px;align-items:start;padding-bottom:8px;width:100%;max-width:980px;margin:0 auto}
2001
2048
  .dab-nav{position:sticky;top:0;display:flex;flex-direction:column;gap:18px}
2002
- .dab-brand{display:flex;align-items:center;gap:10px;padding:2px 6px}
2049
+ /* The brand is an <a> (it opens the repository): an anchor brings its own colour
2050
+ and underline, so both are reset here rather than left to the host's link
2051
+ styles, which would repaint the tile's label. */
2052
+ .dab-brand{display:flex;align-items:center;gap:10px;padding:2px 6px;color:inherit;text-decoration:none}
2053
+ .dab-brand:hover .dab-brand-name{color:var(--dsw-alias-brand-primary)}
2003
2054
  .dab-brand-tile{width:30px;height:30px;flex:none;border-radius:9px;display:grid;place-items:center;color:var(--dsw-alias-label-primary-inverted);background:var(--dsw-alias-brand-primary);box-shadow:0 4px 14px -4px var(--dsw-alias-brand-primary)}
2004
2055
  .dab-brand-name{font-size:13px;font-weight:650;letter-spacing:.01em;line-height:1.25}
2005
2056
  .dab-brand-tag{font-size:9px;letter-spacing:.16em;font-weight:600;color:var(--dsw-alias-label-quaternary,var(--dsw-alias-label-tertiary));text-transform:uppercase}
@@ -2090,6 +2141,11 @@ window.__ModuleLoader__.load({
2090
2141
  .dab-chip-row{display:flex;flex-wrap:wrap;gap:8px}
2091
2142
  .dab-chip{height:30px;padding:0 14px;border-radius:99px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .22s ease}
2092
2143
  .dab-chip:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
2144
+ /* A mode chip on a rule with no picture: there is nothing to lay out. It reads
2145
+ as unavailable rather than as an active choice, and the hover that would
2146
+ promise otherwise is dropped with it. */
2147
+ .dab-chip:disabled{opacity:.45;cursor:not-allowed}
2148
+ .dab-chip:disabled:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
2093
2149
  .dab-chip.is-active{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted);border-color:transparent}
2094
2150
 
2095
2151
  /* ── sliders ─────────────────────────────────────────────────────────────── */
@@ -2148,6 +2204,10 @@ window.__ModuleLoader__.load({
2148
2204
  .dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
2149
2205
  .dab-footer{margin-top:6px;padding:14px 4px 0;border-top:1px solid var(--dsw-alias-border-l2);display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--dsw-alias-label-tertiary)}
2150
2206
  .dab-footer-mono{font-family:var(--dab-mono);letter-spacing:.02em}
2207
+ /* Same reset as the brand: the package name links to the repository, and the
2208
+ underline appears on hover where it reads as an affordance instead of chrome. */
2209
+ .dab-footer a{color:inherit;text-decoration:none}
2210
+ .dab-footer a:hover{color:var(--dsw-alias-brand-primary);text-decoration:underline}
2151
2211
 
2152
2212
  /* ── toast ───────────────────────────────────────────────────────────────── */
2153
2213
  .dab-toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%);display:flex;align-items:center;gap:8px;height:38px;padding:0 16px;border-radius:99px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2);box-shadow:0 10px 30px -8px rgba(0,0,0,.38);font-size:12.5px;z-index:10001;animation:dab-toast-in .32s cubic-bezier(.22,1,.36,1) both}
@@ -2422,6 +2482,10 @@ window.__ModuleLoader__.load({
2422
2482
  "--dsw-menu-surface-fill"
2423
2483
  ]
2424
2484
  },
2485
+ {
2486
+ part: "code",
2487
+ names: ["--dsw-alias-markdown-code-block", "--dsw-alias-markdown-code-block-banner"]
2488
+ },
2425
2489
  {
2426
2490
  part: "input",
2427
2491
  names: ["--dsw-specific-input-major"]
@@ -2435,7 +2499,9 @@ window.__ModuleLoader__.load({
2435
2499
  "--dsw-alias-bg-layer-3": "--dsh-any-op-card-3",
2436
2500
  "--dsw-specific-input-major": "--dsh-any-op-input",
2437
2501
  "--dsw-specific-menu": "--dsh-any-op-menu",
2438
- "--dsw-menu-surface-fill": "--dsh-any-op-menu"
2502
+ "--dsw-menu-surface-fill": "--dsh-any-op-menu",
2503
+ "--dsw-alias-markdown-code-block": "--dsh-any-op-code",
2504
+ "--dsw-alias-markdown-code-block-banner": "--dsh-any-op-code-banner"
2439
2505
  };
2440
2506
  let baseTokenKey = "";
2441
2507
  const HOST_SURFACE_TOKENS = Object.keys(OPACITY_VARS);
@@ -2660,7 +2726,7 @@ window.__ModuleLoader__.load({
2660
2726
  const scheme = own ? forceDark ? "dark" : "light" : resolveHostDark() ? "dark" : "light";
2661
2727
  const fadeable = paletteChanged && paletteFadeAllowed(paletteScheme, scheme);
2662
2728
  lastFadeable = fadeable;
2663
- applyPaletteFade(fadeable);
2729
+ applyPaletteFade(fadeable, Object.keys(tokens));
2664
2730
  if (paletteChanged) {
2665
2731
  paletteScheme = scheme;
2666
2732
  if (own) {
@@ -2672,7 +2738,7 @@ window.__ModuleLoader__.load({
2672
2738
  const opVar = OPACITY_VARS[name];
2673
2739
  decls.push(`${name}:${opVar !== void 0 ? `var(${opVar})` : value}!important`);
2674
2740
  }
2675
- ensureTokenStyle().textContent = `body{${decls.join(";")}}`;
2741
+ ensureTokenStyle().textContent = `:root,body{${decls.join(";")}}`;
2676
2742
  if (own) {
2677
2743
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
2678
2744
  appliedTokenNames = Object.keys(tokens);
@@ -2693,7 +2759,8 @@ window.__ModuleLoader__.load({
2693
2759
  }
2694
2760
  const SETTINGS_PANEL_SEL = "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]";
2695
2761
  const SETTINGS_STYLE_RULE = `${SETTINGS_PANEL_SEL}{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));backdrop-filter:var(--dsh-any-blur-settings,none);}html[${THEMED_ATTR}] ${SETTINGS_PANEL_SEL}{--dsw-alias-bg-layer-1:var(--dsh-any-bg-settings-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-bg-settings-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-bg-settings-layer-3)}${SETTINGS_PANEL_SEL} .dab-card{backdrop-filter:var(--dsh-any-blur-card-panels,none);-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,none)}`;
2696
- const INPUT_BLUR_RULE = "[data-composer-card],[data-cordis-panel]{-webkit-backdrop-filter:var(--dsh-any-input-blur,none);backdrop-filter:var(--dsh-any-input-blur,none)}[data-cordis-panel]{--dsw-specific-menu:var(--dsh-any-op-menu-cordis)!important}";
2762
+ const CORDIS_PANEL_SEL = "[data-cordis-panel]";
2763
+ const INPUT_BLUR_RULE = `[data-composer-card],${CORDIS_PANEL_SEL}{-webkit-backdrop-filter:var(--dsh-any-input-blur,none);backdrop-filter:var(--dsh-any-input-blur,none)}${CORDIS_PANEL_SEL}{--dsw-specific-menu:var(--dsh-any-op-menu-cordis)!important}`;
2697
2764
  function applyInputBlur(px) {
2698
2765
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-input-blur", `blur(${px}px)`);
2699
2766
  else document.documentElement.style.removeProperty("--dsh-any-input-blur");
@@ -2724,20 +2791,82 @@ window.__ModuleLoader__.load({
2724
2791
  write("--dsh-any-traj-layer-2", "--dsw-alias-bg-layer-2");
2725
2792
  write("--dsh-any-traj-layer-3", "--dsw-alias-bg-layer-3");
2726
2793
  }
2794
+ const TRAJECTORY_ROOT_SEL = "[data-conversation-composer-overlay]";
2727
2795
  /** Elements whose own token declarations change when the palette does: the body
2728
2796
  * (which everything else inherits), and the two surfaces that RE-declare the
2729
- * layer tokens instead of inheriting them. */
2730
- const FADE_SELECTORS = [
2731
- "body",
2732
- SETTINGS_PANEL_SEL,
2733
- "[data-conversation-composer-overlay]"
2797
+ * layer tokens instead of inheriting them.
2798
+ *
2799
+ * The three layer tokens are also the ONLY ones those two surfaces may carry a
2800
+ * transition for. Handing them the full palette is not "more of the same": a
2801
+ * transition on an INHERITED token makes the element interpolate towards the
2802
+ * stale value it saw when the fade was armed, and everything under it rides that
2803
+ * lagged copy — the settings dialog's own controls (the holiday switch fill, the
2804
+ * active chips) then hold the previous theme colour for the whole switch and jump
2805
+ * at the end. See ./palette-fade for the measurement. */
2806
+ const FADE_LAYER_TOKENS = [
2807
+ "--dsw-alias-bg-layer-1",
2808
+ "--dsw-alias-bg-layer-2",
2809
+ "--dsw-alias-bg-layer-3"
2734
2810
  ];
2735
- /** The token names the plugin re-emits. One palette is enough: every branch
2736
- * defines the same set, and an unknown name would only be registered when the
2737
- * host can answer for it (see `fadeInitials`). */
2811
+ /** The token names a palette CAN carry — deliberately the widest branch.
2812
+ *
2813
+ * `genTokens(220, 0.55, 0.25)` resolves to the DARK branch (l = 0.25), and that
2814
+ * branch is the superset: the light one emits roughly thirty fewer aliases (no
2815
+ * bubble, selector, tip, tooltip/toast, toolbar and markdown state tokens). This
2816
+ * list is therefore what the REGISTRATIONS are emitted for — a later light→dark
2817
+ * change has to find the dark names already registered — and NOT the transition
2818
+ * list, which is narrowed per apply to the palette in force (`written` in
2819
+ * ./palette-fade). Assuming the two branches carry the same set is exactly the
2820
+ * mistake that let the fade animate host-owned tokens: a name only the dark
2821
+ * branch declares is, under a light palette, the HOST's value.
2822
+ *
2823
+ * An unknown name is only registered when the host can answer for it (see
2824
+ * `fadeInitials`). */
2738
2825
  function fadeTokenNames() {
2739
2826
  return Object.keys(genTokens(220, .55, .25).tokens);
2740
2827
  }
2828
+ /** One `transition` list per element that DECLARES palette tokens, each carrying
2829
+ * only the tokens that element actually declares — and, at emission, only those
2830
+ * the palette in force writes (see `written`).
2831
+ *
2832
+ * Two of the entries carry a STANDARD property instead, because those surfaces
2833
+ * are not painted from a registered token: the settings dialog's plate and the
2834
+ * file-preview panel read a colour the plugin owns as a plain variable
2835
+ * (`--dsh-any-bg-settings-surface`, `--dsh-any-bg-rightbar`) and would otherwise
2836
+ * snap in one frame while every surface around them fades. The Cordis panel is
2837
+ * the third case of the same kind — it re-declares `--dsw-specific-menu` from
2838
+ * `--dsh-any-op-menu-cordis`, which is also unregistered, but since the token it
2839
+ * re-declares IS one, naming the token is what reaches it. */
2840
+ function fadeScopes() {
2841
+ return [
2842
+ {
2843
+ selector: ":root",
2844
+ tokens: fadeTokenNames()
2845
+ },
2846
+ {
2847
+ selector: "body",
2848
+ tokens: fadeTokenNames()
2849
+ },
2850
+ {
2851
+ selector: SETTINGS_PANEL_SEL,
2852
+ tokens: FADE_LAYER_TOKENS,
2853
+ properties: ["background-color"]
2854
+ },
2855
+ {
2856
+ selector: TRAJECTORY_ROOT_SEL,
2857
+ tokens: FADE_LAYER_TOKENS
2858
+ },
2859
+ {
2860
+ selector: RIGHTBAR_SEL,
2861
+ tokens: [],
2862
+ properties: ["background-color"]
2863
+ },
2864
+ {
2865
+ selector: CORDIS_PANEL_SEL,
2866
+ tokens: ["--dsw-specific-menu"]
2867
+ }
2868
+ ];
2869
+ }
2741
2870
  let fadeStyleEl = null;
2742
2871
  let fadeInitials = null;
2743
2872
  let fadeCssCache = "";
@@ -2801,12 +2930,21 @@ window.__ModuleLoader__.load({
2801
2930
  *
2802
2931
  * `animate` is false on every pass that only moves an alpha (a slider drag): the
2803
2932
  * tokens are rewritten there too, and a live transition would make the surface
2804
- * lag behind the pointer. */
2805
- function applyPaletteFade(animate) {
2933
+ * lag behind the pointer.
2934
+ *
2935
+ * `written` is the token set of the palette IN FORCE — `Object.keys(tokens)` of
2936
+ * the very block this pass is about to write — and only those may carry a
2937
+ * transition: a name the palette does not write still belongs to the host, and a
2938
+ * transition on it animates the host's own scheme change at this plugin's
2939
+ * duration (see ./palette-fade for the report that shape produced). The
2940
+ * registrations stay on the full set, so a later palette of the other branch
2941
+ * still has them in place. */
2942
+ function applyPaletteFade(animate, written) {
2806
2943
  const initials = fadeInitialsOf(fadeTokenNames());
2807
2944
  const css = paletteFadeCss({
2808
- selectors: FADE_SELECTORS,
2945
+ scopes: fadeScopes(),
2809
2946
  initial: initials,
2947
+ written,
2810
2948
  durationMs: animate ? paletteFadeMs() : 0,
2811
2949
  easing: rTransition().easing
2812
2950
  });
@@ -2818,7 +2956,8 @@ window.__ModuleLoader__.load({
2818
2956
  function fadeInlineFor(animate) {
2819
2957
  return paletteFadeInline(animate ? paletteFadeMs() : 0, rTransition().easing);
2820
2958
  }
2821
- const RIGHTBAR_STYLE_RULE = "div[data-sidebar-right-panel]:not(:has([data-dockkit-content],[data-dockkit-empty]))[data-sidebar-right-open],div[data-sidebar-right-panel] [data-dockkit-content]:not([data-dockkit-float]),div[data-sidebar-right-panel] [data-dockkit-empty]{background:var(--dsh-any-bg-rightbar,var(--dsw-alias-bg-base))!important}div[data-sidebar-right-panel][data-sidebar-right-open]{-webkit-backdrop-filter:var(--dsh-any-blur-rightbar,none);backdrop-filter:var(--dsh-any-blur-rightbar,none)}";
2959
+ const RIGHTBAR_SEL = "div[data-sidebar-right-panel]:not(:has([data-dockkit-content],[data-dockkit-empty]))[data-sidebar-right-open],div[data-sidebar-right-panel] [data-dockkit-content]:not([data-dockkit-float]),div[data-sidebar-right-panel] [data-dockkit-empty]";
2960
+ const RIGHTBAR_STYLE_RULE = `${RIGHTBAR_SEL}{background:var(--dsh-any-bg-rightbar,var(--dsw-alias-bg-base))!important}div[data-sidebar-right-panel][data-sidebar-right-open]{-webkit-backdrop-filter:var(--dsh-any-blur-rightbar,none);backdrop-filter:var(--dsh-any-blur-rightbar,none)}`;
2822
2961
  /** Paint the file-preview panel's surface from its own slider. */
2823
2962
  function applyRightbarOverrides(op) {
2824
2963
  const root = document.documentElement;
@@ -4018,6 +4157,12 @@ window.__ModuleLoader__.load({
4018
4157
  rx: "2"
4019
4158
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6.6v2M7 8h2" })]
4020
4159
  });
4160
+ /** Angle brackets: the code-block surface on the Interface page. */
4161
+ const CodeIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
4162
+ size,
4163
+ className,
4164
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.7 4.3 2.4 8l3.3 3.7M10.3 4.3 13.6 8l-3.3 3.7" })
4165
+ });
4021
4166
  //#endregion
4022
4167
  //#region src/client/components/ErrorBoundary.tsx
4023
4168
  /**
@@ -4088,6 +4233,19 @@ window.__ModuleLoader__.load({
4088
4233
  return target ? (0, react_dom.createPortal)(children, target) : null;
4089
4234
  }
4090
4235
  //#endregion
4236
+ //#region src/client/repo.ts
4237
+ /**
4238
+ * The project's public repository — the ONE address the shell links to.
4239
+ *
4240
+ * Both links the interface ships (the brand in the header, the package name in
4241
+ * the profile footer) read this constant: two literals would drift the day the
4242
+ * repository moves, and a link that points at the wrong place is indistinguishable
4243
+ * from a link that works until someone clicks it. `scripts/ui-strings-check.ts`
4244
+ * holds it against `package.json`'s `repository`, which is what npm and the
4245
+ * plugin market read.
4246
+ */
4247
+ const REPO_URL = "https://github.com/HarmlessFunny/dsh-background-by-model";
4248
+ //#endregion
4091
4249
  //#region src/client/components/LiveSlider.tsx
4092
4250
  /**
4093
4251
  * Zero-lag slider: the thumb and value label update through DOM refs while
@@ -4184,6 +4342,12 @@ window.__ModuleLoader__.load({
4184
4342
  labelKey: "uiOpacityCard",
4185
4343
  Icon: ChatIcon
4186
4344
  },
4345
+ {
4346
+ opKey: "code",
4347
+ labelKey: "uiOpacityCode",
4348
+ Icon: CodeIcon,
4349
+ noBlur: true
4350
+ },
4187
4351
  {
4188
4352
  opKey: "input",
4189
4353
  labelKey: "uiOpacityInput",
@@ -4227,9 +4391,9 @@ window.__ModuleLoader__.load({
4227
4391
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4228
4392
  className: "dab-grid-parts",
4229
4393
  children: PARTS.map((part, i) => {
4230
- const { labelKey, Icon, isSettings, isChat, isTrajectory, isRightbar } = part;
4394
+ const { labelKey, Icon, isSettings, isChat, isTrajectory, isRightbar, noBlur } = part;
4231
4395
  const opKey = part.opKey;
4232
- const blurKey = isChat ? "chat" : isTrajectory ? "trajectory" : isRightbar ? "rightbar" : isSettings ? "settings" : opKey;
4396
+ const blurKey = isChat ? "chat" : isTrajectory ? "trajectory" : isRightbar ? "rightbar" : isSettings ? "settings" : noBlur ? null : opKey;
4233
4397
  const opacity = isChat ? rChatTextOpacity() : isTrajectory ? rTrajectoryOpacity() : isRightbar ? rRightbarOpacity() : isSettings ? rSop() : rOps()[opKey];
4234
4398
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4235
4399
  className: "dab-card dab-card-hover dab-rise",
@@ -4300,7 +4464,7 @@ window.__ModuleLoader__.load({
4300
4464
  }
4301
4465
  }
4302
4466
  }),
4303
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
4467
+ blurKey !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
4304
4468
  label: t("uiBlur"),
4305
4469
  min: 0,
4306
4470
  max: 60,
@@ -4321,7 +4485,7 @@ window.__ModuleLoader__.load({
4321
4485
  }
4322
4486
  })
4323
4487
  ]
4324
- }, blurKey);
4488
+ }, blurKey ?? opKey);
4325
4489
  })
4326
4490
  })] });
4327
4491
  }
@@ -5399,6 +5563,7 @@ window.__ModuleLoader__.load({
5399
5563
  const rotatable = rule.images.length >= 2;
5400
5564
  const preset = ROTATE_PRESETS.find((x) => x.ms === rule.rotate.intervalMs);
5401
5565
  const selSlot = image?.slot ?? null;
5566
+ const bgMode = image?.bgMode ?? "fit";
5402
5567
  /** How far the pointer must travel before a click turns into a drag. */
5403
5568
  const DRAG_SLOP = 5;
5404
5569
  const tileDown = (i, slot) => (e) => {
@@ -5881,12 +6046,17 @@ window.__ModuleLoader__.load({
5881
6046
  children: t("ruleLayout")
5882
6047
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5883
6048
  className: "dab-chip-row",
6049
+ style: { marginTop: 8 },
5884
6050
  children: [BG_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5885
6051
  type: "button",
5886
- className: `dab-chip${rule.bgMode === m.mode ? " is-active" : ""}`,
5887
- onClick: () => p.setRule(rule.id, { bgMode: m.mode }),
6052
+ className: `dab-chip${bgMode === m.mode ? " is-active" : ""}`,
6053
+ disabled: image === void 0,
6054
+ title: image === void 0 ? t("ruleLayoutNoImage") : void 0,
6055
+ onClick: () => {
6056
+ if (image !== void 0) p.setImageMode(rule.id, image.slot, m.mode);
6057
+ },
5888
6058
  children: t(m.key)
5889
- }, m.mode)), url !== null && rule.bgMode === "fit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6059
+ }, m.mode)), url !== null && bgMode === "fit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5890
6060
  type: "button",
5891
6061
  className: "dab-btn",
5892
6062
  onClick: () => setEditorOpen(true),
@@ -6219,12 +6389,7 @@ window.__ModuleLoader__.load({
6219
6389
  }, easing))
6220
6390
  })] }),
6221
6391
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6222
- className: "dab-swatch-title",
6223
6392
  style: { marginTop: 12 },
6224
- children: t("trDuration")
6225
- }),
6226
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6227
- style: { marginTop: 2 },
6228
6393
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
6229
6394
  label: t("trDuration"),
6230
6395
  min: 0,
@@ -6342,8 +6507,12 @@ window.__ModuleLoader__.load({
6342
6507
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
6343
6508
  className: "dab-footer dab-rise",
6344
6509
  style: { "--d": 4 },
6345
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6510
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
6346
6511
  className: "dab-footer-mono",
6512
+ href: REPO_URL,
6513
+ target: "_blank",
6514
+ rel: "noreferrer noopener",
6515
+ title: t("repoLink"),
6347
6516
  children: "dsh-background-by-model"
6348
6517
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("footerTag") })]
6349
6518
  })
@@ -6407,8 +6576,12 @@ window.__ModuleLoader__.load({
6407
6576
  className: "dab-shell",
6408
6577
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
6409
6578
  className: "dab-nav",
6410
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6579
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
6411
6580
  className: "dab-brand",
6581
+ href: REPO_URL,
6582
+ target: "_blank",
6583
+ rel: "noreferrer noopener",
6584
+ title: t("repoLink"),
6412
6585
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6413
6586
  className: "dab-brand-tile",
6414
6587
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 15 })
@@ -6468,10 +6641,21 @@ window.__ModuleLoader__.load({
6468
6641
  initRpc((endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res));
6469
6642
  let customDispose = null;
6470
6643
  let skinTimer = null;
6644
+ /** Colour scheme of the skin this plugin has registered, or null when none is. */
6645
+ let skinScheme = null;
6646
+ /** Scheme the host currently holds for our theme id; null when it is not there. */
6647
+ const registeredScheme = () => {
6648
+ const theme = ctx.theme.getTheme().themes.find((t) => t.id === CUSTOM_ID);
6649
+ return theme === void 0 ? null : theme.colorScheme;
6650
+ };
6471
6651
  const registerCustom = (h, s, l) => {
6652
+ const { colorScheme, tokens } = genTokens(h, s, l);
6653
+ if (skinScheme === colorScheme && registeredScheme() !== null) {
6654
+ if (ctx.theme.getTheme().preference !== CUSTOM_ID) ctx.theme.setTheme(CUSTOM_ID);
6655
+ return;
6656
+ }
6472
6657
  customDispose?.();
6473
6658
  try {
6474
- const { colorScheme, tokens } = genTokens(h, s, l);
6475
6659
  customDispose = ctx.theme.register({
6476
6660
  id: CUSTOM_ID,
6477
6661
  colorScheme,
@@ -6480,12 +6664,14 @@ window.__ModuleLoader__.load({
6480
6664
  } catch {
6481
6665
  customDispose = null;
6482
6666
  }
6667
+ skinScheme = registeredScheme() ?? colorScheme;
6483
6668
  if (ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
6484
6669
  };
6485
6670
  /** A rule without a saved color means "follow the system theme". */
6486
6671
  const dropCustom = () => {
6487
6672
  customDispose?.();
6488
6673
  customDispose = null;
6674
+ skinScheme = null;
6489
6675
  try {
6490
6676
  if (ctx.theme.getTheme().preference === CUSTOM_ID) ctx.theme.setTheme("system");
6491
6677
  } catch {}
@@ -7040,6 +7226,7 @@ window.__ModuleLoader__.load({
7040
7226
  const slot = nextSlot();
7041
7227
  const image = {
7042
7228
  slot,
7229
+ bgMode: "fit",
7043
7230
  bgState: { ...DEFAULT_BG_STATE },
7044
7231
  color: null
7045
7232
  };
@@ -7060,6 +7247,7 @@ window.__ModuleLoader__.load({
7060
7247
  if (idx < 0) return;
7061
7248
  const winner = winnerId();
7062
7249
  rule.images.splice(idx, 1);
7250
+ if (rule.images.length < 2 && rule.rotate.enabled) patchRotation(rule, { enabled: false });
7063
7251
  if (id === activeRuleId) setRotIndex(0);
7064
7252
  rulesRev++;
7065
7253
  persistConfig();
@@ -7100,6 +7288,29 @@ window.__ModuleLoader__.load({
7100
7288
  saveConfig();
7101
7289
  repaintIfMoved(id, winner);
7102
7290
  },
7291
+ /**
7292
+ * Store ONE image's layout mode. Addressed by slot like `setImageFraming`,
7293
+ * for the same reason: how a picture meets the viewport is a property of the
7294
+ * picture, and 适应 on a tall screenshot says nothing about the landscape
7295
+ * photo beside it in the rotation.
7296
+ *
7297
+ * A mode is a placement, not a paint: it changes nothing about WHICH image is
7298
+ * on screen, only how it is laid out. So the patch is written, sanitized, and
7299
+ * the layer repainted through the same `repaintIfMoved` the framing commit
7300
+ * uses — that call repaints the current image whether or not the winner
7301
+ * moved, which is what makes the chips feel live.
7302
+ */
7303
+ setImageMode: (id, slot, mode) => {
7304
+ const rule = ruleById(id);
7305
+ const image = rule?.images.find((i) => i.slot === slot);
7306
+ if (rule === null || image === void 0) return;
7307
+ const winner = winnerId();
7308
+ image.bgMode = mode;
7309
+ normalizeRuleInPlace(rule);
7310
+ rulesRev++;
7311
+ saveConfig();
7312
+ repaintIfMoved(id, winner);
7313
+ },
7103
7314
  setRuleRotation: (id, patch) => {
7104
7315
  const rule = ruleById(id);
7105
7316
  if (rule === null) return;
@@ -7154,6 +7365,7 @@ window.__ModuleLoader__.load({
7154
7365
  const winner = winnerId();
7155
7366
  rule.images.push({
7156
7367
  slot,
7368
+ bgMode: "fit",
7157
7369
  bgState: { ...DEFAULT_BG_STATE },
7158
7370
  color: null
7159
7371
  });