dsh-background-by-model 0.7.4 → 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,
@@ -1603,10 +1645,11 @@ window.__ModuleLoader__.load({
1603
1645
  if (names.length === 0) return "";
1604
1646
  const css = registrations(names, input.initial);
1605
1647
  if (!(input.durationMs > 0)) return css;
1648
+ const written = new Set(input.written);
1606
1649
  const timing = (n) => `${n} ${input.durationMs}ms ${input.easing}`;
1607
1650
  return `${css}${input.scopes.map((s) => ({
1608
1651
  selector: s.selector,
1609
- transitions: [...s.tokens.filter((n) => names.includes(n)), ...s.properties ?? []]
1652
+ transitions: [...s.tokens.filter((n) => names.includes(n) && written.has(n)), ...s.properties ?? []]
1610
1653
  })).filter((s) => s.transitions.length > 0).map((s) => `${s.selector}{transition:${s.transitions.map(timing).join(",")}}`).join("")}`;
1611
1654
  }
1612
1655
  /**
@@ -1777,6 +1820,7 @@ window.__ModuleLoader__.load({
1777
1820
  "--dsw-alias-interactive-bg-hover": rgba(h(0), s(0), l(-.3), .08),
1778
1821
  "--dsw-alias-interactive-bg-active": rgba(h(0), s(0), l(-.3), .14),
1779
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)),
1780
1824
  "--dsw-alias-markdown-inline-code": hsl(h(0), s(-.08), l(.04)),
1781
1825
  "--dsw-specific-sidebar-fill": hsl(h(0), s(-.1), l(-.03)),
1782
1826
  "--dsw-specific-sidebar-nav-item-active": hsl(h(0), s(-.08), l(.05)),
@@ -2002,7 +2046,11 @@ window.__ModuleLoader__.load({
2002
2046
  /* ── shell: nav rail + page body ─────────────────────────────────────────── */
2003
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}
2004
2048
  .dab-nav{position:sticky;top:0;display:flex;flex-direction:column;gap:18px}
2005
- .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)}
2006
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)}
2007
2055
  .dab-brand-name{font-size:13px;font-weight:650;letter-spacing:.01em;line-height:1.25}
2008
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}
@@ -2093,6 +2141,11 @@ window.__ModuleLoader__.load({
2093
2141
  .dab-chip-row{display:flex;flex-wrap:wrap;gap:8px}
2094
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}
2095
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))}
2096
2149
  .dab-chip.is-active{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted);border-color:transparent}
2097
2150
 
2098
2151
  /* ── sliders ─────────────────────────────────────────────────────────────── */
@@ -2151,6 +2204,10 @@ window.__ModuleLoader__.load({
2151
2204
  .dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
2152
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)}
2153
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}
2154
2211
 
2155
2212
  /* ── toast ───────────────────────────────────────────────────────────────── */
2156
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}
@@ -2425,6 +2482,10 @@ window.__ModuleLoader__.load({
2425
2482
  "--dsw-menu-surface-fill"
2426
2483
  ]
2427
2484
  },
2485
+ {
2486
+ part: "code",
2487
+ names: ["--dsw-alias-markdown-code-block", "--dsw-alias-markdown-code-block-banner"]
2488
+ },
2428
2489
  {
2429
2490
  part: "input",
2430
2491
  names: ["--dsw-specific-input-major"]
@@ -2438,7 +2499,9 @@ window.__ModuleLoader__.load({
2438
2499
  "--dsw-alias-bg-layer-3": "--dsh-any-op-card-3",
2439
2500
  "--dsw-specific-input-major": "--dsh-any-op-input",
2440
2501
  "--dsw-specific-menu": "--dsh-any-op-menu",
2441
- "--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"
2442
2505
  };
2443
2506
  let baseTokenKey = "";
2444
2507
  const HOST_SURFACE_TOKENS = Object.keys(OPACITY_VARS);
@@ -2663,7 +2726,7 @@ window.__ModuleLoader__.load({
2663
2726
  const scheme = own ? forceDark ? "dark" : "light" : resolveHostDark() ? "dark" : "light";
2664
2727
  const fadeable = paletteChanged && paletteFadeAllowed(paletteScheme, scheme);
2665
2728
  lastFadeable = fadeable;
2666
- applyPaletteFade(fadeable);
2729
+ applyPaletteFade(fadeable, Object.keys(tokens));
2667
2730
  if (paletteChanged) {
2668
2731
  paletteScheme = scheme;
2669
2732
  if (own) {
@@ -2675,7 +2738,7 @@ window.__ModuleLoader__.load({
2675
2738
  const opVar = OPACITY_VARS[name];
2676
2739
  decls.push(`${name}:${opVar !== void 0 ? `var(${opVar})` : value}!important`);
2677
2740
  }
2678
- ensureTokenStyle().textContent = `body{${decls.join(";")}}`;
2741
+ ensureTokenStyle().textContent = `:root,body{${decls.join(";")}}`;
2679
2742
  if (own) {
2680
2743
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
2681
2744
  appliedTokenNames = Object.keys(tokens);
@@ -2745,14 +2808,26 @@ window.__ModuleLoader__.load({
2745
2808
  "--dsw-alias-bg-layer-2",
2746
2809
  "--dsw-alias-bg-layer-3"
2747
2810
  ];
2748
- /** The token names the plugin re-emits. One palette is enough: every branch
2749
- * defines the same set, and an unknown name would only be registered when the
2750
- * 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`). */
2751
2825
  function fadeTokenNames() {
2752
2826
  return Object.keys(genTokens(220, .55, .25).tokens);
2753
2827
  }
2754
2828
  /** One `transition` list per element that DECLARES palette tokens, each carrying
2755
- * only the tokens that element actually declares.
2829
+ * only the tokens that element actually declares — and, at emission, only those
2830
+ * the palette in force writes (see `written`).
2756
2831
  *
2757
2832
  * Two of the entries carry a STANDARD property instead, because those surfaces
2758
2833
  * are not painted from a registered token: the settings dialog's plate and the
@@ -2764,6 +2839,10 @@ window.__ModuleLoader__.load({
2764
2839
  * re-declares IS one, naming the token is what reaches it. */
2765
2840
  function fadeScopes() {
2766
2841
  return [
2842
+ {
2843
+ selector: ":root",
2844
+ tokens: fadeTokenNames()
2845
+ },
2767
2846
  {
2768
2847
  selector: "body",
2769
2848
  tokens: fadeTokenNames()
@@ -2851,12 +2930,21 @@ window.__ModuleLoader__.load({
2851
2930
  *
2852
2931
  * `animate` is false on every pass that only moves an alpha (a slider drag): the
2853
2932
  * tokens are rewritten there too, and a live transition would make the surface
2854
- * lag behind the pointer. */
2855
- 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) {
2856
2943
  const initials = fadeInitialsOf(fadeTokenNames());
2857
2944
  const css = paletteFadeCss({
2858
2945
  scopes: fadeScopes(),
2859
2946
  initial: initials,
2947
+ written,
2860
2948
  durationMs: animate ? paletteFadeMs() : 0,
2861
2949
  easing: rTransition().easing
2862
2950
  });
@@ -4069,6 +4157,12 @@ window.__ModuleLoader__.load({
4069
4157
  rx: "2"
4070
4158
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6.6v2M7 8h2" })]
4071
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
+ });
4072
4166
  //#endregion
4073
4167
  //#region src/client/components/ErrorBoundary.tsx
4074
4168
  /**
@@ -4139,6 +4233,19 @@ window.__ModuleLoader__.load({
4139
4233
  return target ? (0, react_dom.createPortal)(children, target) : null;
4140
4234
  }
4141
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
4142
4249
  //#region src/client/components/LiveSlider.tsx
4143
4250
  /**
4144
4251
  * Zero-lag slider: the thumb and value label update through DOM refs while
@@ -4235,6 +4342,12 @@ window.__ModuleLoader__.load({
4235
4342
  labelKey: "uiOpacityCard",
4236
4343
  Icon: ChatIcon
4237
4344
  },
4345
+ {
4346
+ opKey: "code",
4347
+ labelKey: "uiOpacityCode",
4348
+ Icon: CodeIcon,
4349
+ noBlur: true
4350
+ },
4238
4351
  {
4239
4352
  opKey: "input",
4240
4353
  labelKey: "uiOpacityInput",
@@ -4278,9 +4391,9 @@ window.__ModuleLoader__.load({
4278
4391
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4279
4392
  className: "dab-grid-parts",
4280
4393
  children: PARTS.map((part, i) => {
4281
- const { labelKey, Icon, isSettings, isChat, isTrajectory, isRightbar } = part;
4394
+ const { labelKey, Icon, isSettings, isChat, isTrajectory, isRightbar, noBlur } = part;
4282
4395
  const opKey = part.opKey;
4283
- 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;
4284
4397
  const opacity = isChat ? rChatTextOpacity() : isTrajectory ? rTrajectoryOpacity() : isRightbar ? rRightbarOpacity() : isSettings ? rSop() : rOps()[opKey];
4285
4398
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4286
4399
  className: "dab-card dab-card-hover dab-rise",
@@ -4351,7 +4464,7 @@ window.__ModuleLoader__.load({
4351
4464
  }
4352
4465
  }
4353
4466
  }),
4354
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
4467
+ blurKey !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
4355
4468
  label: t("uiBlur"),
4356
4469
  min: 0,
4357
4470
  max: 60,
@@ -4372,7 +4485,7 @@ window.__ModuleLoader__.load({
4372
4485
  }
4373
4486
  })
4374
4487
  ]
4375
- }, blurKey);
4488
+ }, blurKey ?? opKey);
4376
4489
  })
4377
4490
  })] });
4378
4491
  }
@@ -5450,6 +5563,7 @@ window.__ModuleLoader__.load({
5450
5563
  const rotatable = rule.images.length >= 2;
5451
5564
  const preset = ROTATE_PRESETS.find((x) => x.ms === rule.rotate.intervalMs);
5452
5565
  const selSlot = image?.slot ?? null;
5566
+ const bgMode = image?.bgMode ?? "fit";
5453
5567
  /** How far the pointer must travel before a click turns into a drag. */
5454
5568
  const DRAG_SLOP = 5;
5455
5569
  const tileDown = (i, slot) => (e) => {
@@ -5932,12 +6046,17 @@ window.__ModuleLoader__.load({
5932
6046
  children: t("ruleLayout")
5933
6047
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5934
6048
  className: "dab-chip-row",
6049
+ style: { marginTop: 8 },
5935
6050
  children: [BG_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5936
6051
  type: "button",
5937
- className: `dab-chip${rule.bgMode === m.mode ? " is-active" : ""}`,
5938
- 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
+ },
5939
6058
  children: t(m.key)
5940
- }, 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", {
5941
6060
  type: "button",
5942
6061
  className: "dab-btn",
5943
6062
  onClick: () => setEditorOpen(true),
@@ -6270,12 +6389,7 @@ window.__ModuleLoader__.load({
6270
6389
  }, easing))
6271
6390
  })] }),
6272
6391
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6273
- className: "dab-swatch-title",
6274
6392
  style: { marginTop: 12 },
6275
- children: t("trDuration")
6276
- }),
6277
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6278
- style: { marginTop: 2 },
6279
6393
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
6280
6394
  label: t("trDuration"),
6281
6395
  min: 0,
@@ -6393,8 +6507,12 @@ window.__ModuleLoader__.load({
6393
6507
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
6394
6508
  className: "dab-footer dab-rise",
6395
6509
  style: { "--d": 4 },
6396
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6510
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
6397
6511
  className: "dab-footer-mono",
6512
+ href: REPO_URL,
6513
+ target: "_blank",
6514
+ rel: "noreferrer noopener",
6515
+ title: t("repoLink"),
6398
6516
  children: "dsh-background-by-model"
6399
6517
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("footerTag") })]
6400
6518
  })
@@ -6458,8 +6576,12 @@ window.__ModuleLoader__.load({
6458
6576
  className: "dab-shell",
6459
6577
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
6460
6578
  className: "dab-nav",
6461
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6579
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
6462
6580
  className: "dab-brand",
6581
+ href: REPO_URL,
6582
+ target: "_blank",
6583
+ rel: "noreferrer noopener",
6584
+ title: t("repoLink"),
6463
6585
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6464
6586
  className: "dab-brand-tile",
6465
6587
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 15 })
@@ -6519,10 +6641,21 @@ window.__ModuleLoader__.load({
6519
6641
  initRpc((endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res));
6520
6642
  let customDispose = null;
6521
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
+ };
6522
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
+ }
6523
6657
  customDispose?.();
6524
6658
  try {
6525
- const { colorScheme, tokens } = genTokens(h, s, l);
6526
6659
  customDispose = ctx.theme.register({
6527
6660
  id: CUSTOM_ID,
6528
6661
  colorScheme,
@@ -6531,12 +6664,14 @@ window.__ModuleLoader__.load({
6531
6664
  } catch {
6532
6665
  customDispose = null;
6533
6666
  }
6667
+ skinScheme = registeredScheme() ?? colorScheme;
6534
6668
  if (ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
6535
6669
  };
6536
6670
  /** A rule without a saved color means "follow the system theme". */
6537
6671
  const dropCustom = () => {
6538
6672
  customDispose?.();
6539
6673
  customDispose = null;
6674
+ skinScheme = null;
6540
6675
  try {
6541
6676
  if (ctx.theme.getTheme().preference === CUSTOM_ID) ctx.theme.setTheme("system");
6542
6677
  } catch {}
@@ -7091,6 +7226,7 @@ window.__ModuleLoader__.load({
7091
7226
  const slot = nextSlot();
7092
7227
  const image = {
7093
7228
  slot,
7229
+ bgMode: "fit",
7094
7230
  bgState: { ...DEFAULT_BG_STATE },
7095
7231
  color: null
7096
7232
  };
@@ -7111,6 +7247,7 @@ window.__ModuleLoader__.load({
7111
7247
  if (idx < 0) return;
7112
7248
  const winner = winnerId();
7113
7249
  rule.images.splice(idx, 1);
7250
+ if (rule.images.length < 2 && rule.rotate.enabled) patchRotation(rule, { enabled: false });
7114
7251
  if (id === activeRuleId) setRotIndex(0);
7115
7252
  rulesRev++;
7116
7253
  persistConfig();
@@ -7151,6 +7288,29 @@ window.__ModuleLoader__.load({
7151
7288
  saveConfig();
7152
7289
  repaintIfMoved(id, winner);
7153
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
+ },
7154
7314
  setRuleRotation: (id, patch) => {
7155
7315
  const rule = ruleById(id);
7156
7316
  if (rule === null) return;
@@ -7205,6 +7365,7 @@ window.__ModuleLoader__.load({
7205
7365
  const winner = winnerId();
7206
7366
  rule.images.push({
7207
7367
  slot,
7368
+ bgMode: "fit",
7208
7369
  bgState: { ...DEFAULT_BG_STATE },
7209
7370
  color: null
7210
7371
  });