dsh-background-by-model 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -26,7 +26,7 @@ window.__ModuleLoader__.load({
26
26
  pageProfile: "配置",
27
27
  descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感。这里的设置对所有模型生效",
28
28
  descModelBg: "每条规则 = 一个匹配串 + 一组背景图与外观。切换模型时,从上往下找第一条匹配串出现在模型名里的规则;都没中就使用第 1 条。一条规则可以放多张图,按你设的节奏轮换",
29
- descProfile: "把当前全部规则连同图片导出备份,或从文件一键恢复",
29
+ descProfile: "调整壁纸切换效果,并把当前全部规则连同图片导出备份,或从文件一键恢复",
30
30
  uiTitle: "主界面",
31
31
  uiOpacity: "透明度",
32
32
  uiBlur: "模糊度",
@@ -142,7 +142,22 @@ window.__ModuleLoader__.load({
142
142
  importFail: "导入失败,文件格式不正确",
143
143
  footerTag: "按模型换背景插件",
144
144
  holidayTitle: "节日特殊背景",
145
- holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)"
145
+ holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)",
146
+ trTitle: "切换效果",
147
+ trEffect: "切换方式",
148
+ trEffectFade: "淡入淡出",
149
+ trEffectNone: "直接切换",
150
+ trEffectZoom: "缩放淡入",
151
+ trEffectSlide: "平移推入",
152
+ trEasing: "缓动",
153
+ trEasingEase: "标准",
154
+ trEasingLinear: "匀速",
155
+ trEasingOut: "先快后慢",
156
+ trEasingInOut: "两端慢",
157
+ trDuration: "切换时长",
158
+ trPreview: "试放",
159
+ trHint: "效果对所有换图生效:切模型、轮换、手动下一张、节日。「直接切换」和 0 秒一样是硬切",
160
+ trReducedHint: "系统开启了「减少动态效果」,实际切换不会播放动画"
146
161
  };
147
162
  const en = {
148
163
  nav: "Model background",
@@ -152,7 +167,7 @@ window.__ModuleLoader__.load({
152
167
  pageProfile: "Profile",
153
168
  descInterface: "Tune opacity and blur per surface to build depth. These settings are global and apply to every model",
154
169
  descModelBg: "Each rule is a match string plus a set of wallpapers and one look. On a model switch the list is scanned top-down for the first match string contained in the model name; if nothing hits, rule 1 is used. A rule can hold several images and rotate through them on a rhythm you set",
155
- descProfile: "Back up every rule together with its image, or restore one from a file",
170
+ descProfile: "Tune how the wallpaper switches, and back up every rule together with its image — or restore one from a file",
156
171
  uiTitle: "Interface",
157
172
  uiOpacity: "Opacity",
158
173
  uiBlur: "Blur",
@@ -268,9 +283,106 @@ window.__ModuleLoader__.load({
268
283
  importFail: "Import failed — invalid file",
269
284
  footerTag: "Per-model wallpaper plugin",
270
285
  holidayTitle: "Holiday backgrounds",
271
- holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)"
286
+ holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)",
287
+ trTitle: "Switch effect",
288
+ trEffect: "Transition",
289
+ trEffectFade: "Cross-fade",
290
+ trEffectNone: "Instant",
291
+ trEffectZoom: "Zoom in",
292
+ trEffectSlide: "Slide in",
293
+ trEasing: "Easing",
294
+ trEasingEase: "Standard",
295
+ trEasingLinear: "Linear",
296
+ trEasingOut: "Ease out",
297
+ trEasingInOut: "Ease in-out",
298
+ trDuration: "Duration",
299
+ trPreview: "Preview",
300
+ trHint: "The effect applies to every change: model switch, rotation, manual next, holidays. “Instant” and 0 s are the same hard cut",
301
+ trReducedHint: "Your system asks for reduced motion, so a real switch will not animate"
272
302
  };
273
303
  //#endregion
304
+ //#region src/client/transition.ts
305
+ /**
306
+ * Resolve the duration one switch uses, in ms (`0` = a hard cut).
307
+ *
308
+ * 1. `effect: 'none'` is a hard cut whatever the duration says — asking for no
309
+ * animation and asking for 0 ms are the same request, and the effect is the
310
+ * more explicit of the two;
311
+ * 2. otherwise the global `durationMs` decides, because a rule has no notion of
312
+ * a transition and there is exactly one duration for every switch.
313
+ *
314
+ * A non-finite or negative value answers `0` (a hard cut) rather than a default:
315
+ * animating for an unknown length is the one answer that cannot be right. The
316
+ * input is already clamped by the shared sanitizer (`./schema`), so this is a
317
+ * guard against a hand-edited config, not a second clamp.
318
+ */
319
+ function resolveFadeMs(transition) {
320
+ if (transition.effect === "none") return 0;
321
+ return Number.isFinite(transition.durationMs) && transition.durationMs > 0 ? transition.durationMs : 0;
322
+ }
323
+ /**
324
+ * The transform the incoming layer starts from, per effect.
325
+ *
326
+ * `zoom` starts ABOVE 1 and settles back to exactly `none`, never below or past
327
+ * it: the wallpaper would otherwise be left larger than the viewport, cropping a
328
+ * `fit` framing the user committed by hand. `slide` is a small push because a
329
+ * full-width travel of a full-bleed wallpaper reads as two wallpapers, not one
330
+ * transition.
331
+ */
332
+ const START_TRANSFORM = {
333
+ fade: "none",
334
+ none: "none",
335
+ zoom: "scale(1.06)",
336
+ slide: "translateX(6%)"
337
+ };
338
+ /** The properties one effect actually animates — an unchanged property must not
339
+ * be put on the transition list, or `fade` would stop being byte-for-byte the
340
+ * cross-fade every release so far performed. */
341
+ function animatedProps(effect) {
342
+ return effect === "fade" ? ["opacity"] : ["opacity", "transform"];
343
+ }
344
+ /**
345
+ * Decide one switch: whether to animate, from what, to what, and on what curve.
346
+ *
347
+ * `canAnimate` is the caller's answer to "is there anything to animate from" —
348
+ * a first paint has no previous layer, and a wallpaper at 0% opacity is not on
349
+ * screen — plus the window-visibility question the caller already owns. It is
350
+ * passed in rather than computed here so this stays a pure function of its
351
+ * inputs.
352
+ *
353
+ * `reducedMotion` is honoured as a hard veto rather than a shortened duration:
354
+ * the point of the preference is that the interface stops moving, and a fast
355
+ * animation is still an animation.
356
+ */
357
+ function transitionPlan(input) {
358
+ const still = {
359
+ opacity: "1",
360
+ transform: "none"
361
+ };
362
+ const moves = input.durationMs > 0;
363
+ if (!input.canAnimate || input.reducedMotion || input.effect === "none" || !moves) return {
364
+ animate: false,
365
+ durationMs: 0,
366
+ from: still,
367
+ to: still,
368
+ transition: "none",
369
+ willChange: ""
370
+ };
371
+ const props = animatedProps(input.effect);
372
+ const timing = `${input.durationMs}ms ${input.easing}`;
373
+ return {
374
+ animate: true,
375
+ durationMs: input.durationMs,
376
+ from: {
377
+ opacity: "0",
378
+ transform: START_TRANSFORM[input.effect]
379
+ },
380
+ to: still,
381
+ transition: props.map((p) => `${p} ${timing}`).join(", "),
382
+ willChange: props.join(", ")
383
+ };
384
+ }
385
+ //#endregion
274
386
  //#region src/holiday.ts
275
387
  const HOLIDAYS = [{
276
388
  id: "mid-autumn",
@@ -449,15 +561,12 @@ window.__ModuleLoader__.load({
449
561
  const ROTATE_MIN_MS = 5e3;
450
562
  /** Slowest allowed dwell time (24 h): one image per day is still "rotation". */
451
563
  const ROTATE_MAX_MS = 864e5;
452
- /** Longest allowed cross-fade; 0 is a hard cut. */
453
- const ROTATE_FADE_MAX_MS = 3e3;
454
564
  /** Everything off: the shipped default, and what a config predating this feature gets. */
455
565
  const DEFAULT_ROTATION = {
456
566
  enabled: false,
457
567
  intervalMs: 6e4,
458
568
  order: "order",
459
- advanceOnSwitch: false,
460
- fadeMs: 320
569
+ advanceOnSwitch: false
461
570
  };
462
571
  /** A fresh rotation block (never hand out the shared default object). */
463
572
  function defaultRotation() {
@@ -514,6 +623,32 @@ window.__ModuleLoader__.load({
514
623
  rightbar: 0,
515
624
  input: 0
516
625
  };
626
+ /** Longest allowed switch duration; 0 is a hard cut. */
627
+ const TRANSITION_MAX_MS = 3e3;
628
+ /** Every accepted `effect`, in UI order. */
629
+ const TRANSITION_EFFECTS = [
630
+ "fade",
631
+ "none",
632
+ "zoom",
633
+ "slide"
634
+ ];
635
+ /** Every accepted `easing`, in UI order. */
636
+ const TRANSITION_EASINGS = [
637
+ "ease",
638
+ "linear",
639
+ "ease-out",
640
+ "ease-in-out"
641
+ ];
642
+ /** The shipped default: the cross-fade this plugin already performed, at 320 ms. */
643
+ const DEFAULT_TRANSITION = {
644
+ effect: "fade",
645
+ easing: "ease",
646
+ durationMs: 320
647
+ };
648
+ /** A fresh transition block (never hand out the shared default object). */
649
+ function defaultTransition() {
650
+ return { ...DEFAULT_TRANSITION };
651
+ }
517
652
  /** Every slot a rule owns, in order. The one thing the render and store layers
518
653
  * ask of the image list, so a rule's slots are never enumerated ad hoc. */
519
654
  function ruleSlots(rule) {
@@ -617,6 +752,7 @@ window.__ModuleLoader__.load({
617
752
  rules: [],
618
753
  opacities: { ...DEFAULT_PART_OPACITIES },
619
754
  blurs: { ...DEFAULT_PART_BLURS },
755
+ transition: defaultTransition(),
620
756
  settingsOpacity: 1,
621
757
  chatTextOpacity: 0,
622
758
  trajectoryOpacity: 1,
@@ -662,8 +798,7 @@ window.__ModuleLoader__.load({
662
798
  enabled: r.enabled === true,
663
799
  intervalMs: clamp$1(r.intervalMs, ROTATE_MIN_MS, ROTATE_MAX_MS, d.intervalMs),
664
800
  order: ROTATE_ORDERS.includes(r.order) ? r.order : d.order,
665
- advanceOnSwitch: r.advanceOnSwitch === true,
666
- fadeMs: clamp$1(r.fadeMs, 0, ROTATE_FADE_MAX_MS, d.fadeMs)
801
+ advanceOnSwitch: r.advanceOnSwitch === true
667
802
  };
668
803
  }
669
804
  /** Own-property test for values that may be anything at all (a raw JSON entry). */
@@ -671,6 +806,54 @@ window.__ModuleLoader__.load({
671
806
  return Object.prototype.hasOwnProperty.call(o, key);
672
807
  }
673
808
  /**
809
+ * The duration a config written before the switch effect carries on any of its
810
+ * rules, or `undefined` when it carries none worth adopting.
811
+ *
812
+ * `rotate.fadeMs` was the per-rule duration from 0.7.0 on, and it had NO control
813
+ * anywhere — it could only be hand-edited, which is why the global setting
814
+ * replaced it. This is the one-shot lift that keeps such a hand-tuned value from
815
+ * being silently dropped on upgrade: the first rule whose value differs from the
816
+ * 320 ms every build shipped is adopted as the global duration. Values that are
817
+ * all the default are ignored, so an ordinary config keeps the ordinary default,
818
+ * and rules that DISAGREE cannot be represented by one global number — the first
819
+ * deliberate value wins, and the others are gone with the field they lived on.
820
+ */
821
+ function legacyFadeMs(rawRules) {
822
+ if (!Array.isArray(rawRules)) return void 0;
823
+ for (const entry of rawRules) {
824
+ const rotate = entry?.rotate;
825
+ if (rotate === null || typeof rotate !== "object") continue;
826
+ const value = rotate.fadeMs;
827
+ if (typeof value !== "number" || !isFinite(value)) continue;
828
+ if (value === DEFAULT_TRANSITION.durationMs) continue;
829
+ return clamp$1(value, 0, TRANSITION_MAX_MS, DEFAULT_TRANSITION.durationMs);
830
+ }
831
+ }
832
+ /**
833
+ * Coerce one persisted transition block; a config written before this setting
834
+ * existed has no `transition` key at all and gets the shipped default, which is
835
+ * the behaviour it already had.
836
+ *
837
+ * `legacyMs` is the per-rule `fadeMs` this shape replaced (see `legacyFadeMs`),
838
+ * adopted only when the incoming config has no `durationMs` of its own — so a
839
+ * deliberate hand-tuned blend survives the upgrade instead of being dropped with
840
+ * the field it used to live on.
841
+ *
842
+ * `durationMs` is clamped with `0` kept as a real value (a hard cut) rather than
843
+ * read as "absent": 0 is exactly how a hard cut is asked for without changing
844
+ * the effect.
845
+ */
846
+ function normalizeTransition(raw, legacyMs) {
847
+ const t = raw ?? {};
848
+ const d = DEFAULT_TRANSITION;
849
+ const fallback = typeof legacyMs === "number" && isFinite(legacyMs) ? legacyMs : d.durationMs;
850
+ return {
851
+ effect: TRANSITION_EFFECTS.includes(t.effect) ? t.effect : d.effect,
852
+ easing: TRANSITION_EASINGS.includes(t.easing) ? t.easing : d.easing,
853
+ durationMs: clamp$1(t.durationMs, 0, TRANSITION_MAX_MS, fallback)
854
+ };
855
+ }
856
+ /**
674
857
  * Coerce one persisted image entry, or null when it lacks a usable slot.
675
858
  *
676
859
  * `inherit` is the RULE's own color, and it is applied only to an entry that has
@@ -807,6 +990,7 @@ window.__ModuleLoader__.load({
807
990
  rules,
808
991
  opacities,
809
992
  blurs,
993
+ transition: normalizeTransition(r.transition, legacyFadeMs(r.rules)),
810
994
  settingsOpacity: clamp01(r.settingsOpacity, 1),
811
995
  chatTextOpacity: clamp01(r.chatTextOpacity, 0),
812
996
  trajectoryOpacity: clamp01(r.trajectoryOpacity, 1),
@@ -1084,13 +1268,26 @@ window.__ModuleLoader__.load({
1084
1268
  function rBgState() {
1085
1269
  return activeImage()?.bgState ?? DEFAULT_BG_STATE;
1086
1270
  }
1087
- /** Rotation of the active rule; the shipped default when nothing is active. */
1088
- function rRotation() {
1089
- return activeRule()?.rotate ?? defaultRotation();
1271
+ /**
1272
+ * The GLOBAL wallpaper-switch transition (effect, easing, duration).
1273
+ *
1274
+ * Read from the config on every apply rather than cached: the Config page edits
1275
+ * it while a wallpaper is on screen, and the next switch has to use what the
1276
+ * user just chose.
1277
+ */
1278
+ function rTransition() {
1279
+ return cfg.transition ?? DEFAULT_TRANSITION;
1090
1280
  }
1091
- /** Cross-fade duration the active rule asks for, in ms (0 = hard cut). */
1281
+ /**
1282
+ * Duration the next switch runs for, in ms (`0` = a hard cut).
1283
+ *
1284
+ * The two rungs are `resolveFadeMs`'s business (./transition): the effect can
1285
+ * veto the animation outright, and otherwise the global duration decides. It is
1286
+ * deliberately NOT a per-rule value — a rule has no notion of a transition, and
1287
+ * the per-rule field this replaced had no control anywhere to set it with.
1288
+ */
1092
1289
  function rFadeMs() {
1093
- return rRotation().fadeMs;
1290
+ return resolveFadeMs(rTransition());
1094
1291
  }
1095
1292
  /** Paintable URL of the active rule's CURRENT image, or null when it has none. */
1096
1293
  function rWp() {
@@ -1313,7 +1510,7 @@ window.__ModuleLoader__.load({
1313
1510
  const d = data;
1314
1511
  const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
1315
1512
  const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
1316
- hostShape = schema === null ? "legacy" : schema >= 4 ? "current" : "legacy";
1513
+ hostShape = schema === null ? "legacy" : schema >= 5 ? "current" : "legacy";
1317
1514
  if (hostShape === "legacy") holdWrite();
1318
1515
  return {
1319
1516
  config: d.config,
@@ -1371,6 +1568,57 @@ window.__ModuleLoader__.load({
1371
1568
  return parts.length > 0 ? parts.join(" ") : null;
1372
1569
  }
1373
1570
  //#endregion
1571
+ //#region src/client/palette-fade.ts
1572
+ /**
1573
+ * Whether a palette change may interpolate at all.
1574
+ *
1575
+ * Only while the SCHEME stays put. A light↔dark flip is not a tint of the same
1576
+ * palette, it is a different palette: interpolating between them walks the whole
1577
+ * interface through mid-tones whose ink is wrong in both directions (dark text
1578
+ * half-way into a dark palette), which looks worse than the switch it replaces.
1579
+ * The wallpaper has no such problem — one image fades into another either way —
1580
+ * so its own animation is deliberately untouched by this.
1581
+ *
1582
+ * `previous === null` means nothing has been painted yet (the first apply after a
1583
+ * load): there is no colour to come from, so there is nothing to fade.
1584
+ */
1585
+ function paletteFadeAllowed(previous, next) {
1586
+ return previous === next;
1587
+ }
1588
+ /** One `@property` block per registered token. */
1589
+ function registrations(names, initial) {
1590
+ return names.map((n) => `@property ${n}{syntax:'<color>';inherits:true;initial-value:${initial[n]}}`).join("");
1591
+ }
1592
+ /**
1593
+ * The stylesheet for one state: the registrations always, the transition only
1594
+ * while a palette change is actually being painted.
1595
+ *
1596
+ * `durationMs = 0` deliberately emits NO transition rule. That is not only
1597
+ * "instant": it is what keeps a slider drag instant, because the same call sites
1598
+ * rewrite the `body{…}` rule on every frame of a drag and a live transition on
1599
+ * those tokens would make the alphas lag behind the pointer.
1600
+ */
1601
+ function paletteFadeCss(input) {
1602
+ const names = Object.keys(input.initial).filter((n) => n.startsWith("--"));
1603
+ if (names.length === 0) return "";
1604
+ 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}}`;
1608
+ }
1609
+ /**
1610
+ * The same fade for an element the plugin paints INLINE (its columns and cards).
1611
+ *
1612
+ * No custom property is involved there — the element's own `background-color` is
1613
+ * what changes — so the standard property is what has to be told. `0` means
1614
+ * `none` rather than an empty value: an empty value would leave whatever the host
1615
+ * had declared on that element, and the point of the 0 case is that a drag must
1616
+ * not fade at all.
1617
+ */
1618
+ function paletteFadeInline(durationMs, easing) {
1619
+ return durationMs > 0 ? `background-color ${durationMs}ms ${easing}` : "none";
1620
+ }
1621
+ //#endregion
1374
1622
  //#region src/client/utils/color.ts
1375
1623
  function hsvToHsl(h, s, v) {
1376
1624
  const l = v * (1 - s / 2);
@@ -1890,6 +2138,11 @@ window.__ModuleLoader__.load({
1890
2138
 
1891
2139
  /* ── profile page ────────────────────────────────────────────────────────── */
1892
2140
  .dab-profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(238px,1fr));gap:13px}
2141
+ /* The switch-effect preview: a two-tone swatch that replays the chosen effect in
2142
+ the panel. It exists because a wallpaper fading into ITSELF is invisible —
2143
+ the one thing a transition setting must not leave the user guessing about. */
2144
+ .dab-tr-preview{position:relative;overflow:hidden;flex:1;min-width:0;height:74px;border-radius:12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);background-image:linear-gradient(120deg,color-mix(in srgb,var(--dsw-alias-label-tertiary) 24%,transparent),transparent 70%)}
2145
+ .dab-tr-preview-in{position:absolute;inset:0;background-image:linear-gradient(120deg,var(--dsw-alias-brand-primary),color-mix(in srgb,var(--dsw-alias-brand-primary) 50%,var(--dsw-alias-bg-layer-1)))}
1893
2146
  .dab-profile-ico{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;margin-bottom:13px;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
1894
2147
  .dab-profile-title{font-size:14px;font-weight:650}
1895
2148
  .dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
@@ -2323,19 +2576,70 @@ window.__ModuleLoader__.load({
2323
2576
  }
2324
2577
  let pendingOps = null;
2325
2578
  let tokensRaf = null;
2579
+ /** When the pending frame was armed, so a frame that never ran can be spotted. */
2580
+ let tokensArmedAt = 0;
2581
+ /** A frame outstanding for longer than this is treated as lost (see below). */
2582
+ const STALE_TOKENS_RAF_MS = 1e3;
2583
+ /** The fingerprint of the palette + colour in force; see `applyCustomTokensNow`. */
2584
+ function paletteKey(src, color) {
2585
+ return src.own ? `own:${color[0]}|${color[1]}|${color[2]}` : `host:${HOST_SURFACE_TOKENS.map((n) => src.tokens[n] ?? "").join("|")}`;
2586
+ }
2587
+ /**
2588
+ * Re-emit the interface palette, coalescing a slider drag into one frame.
2589
+ *
2590
+ * A PALETTE change is never coalesced, and that distinction is the whole point:
2591
+ * this call is the only writer of the palette's INK. Every other surface has a
2592
+ * second, synchronous writer (the dialog's layer tokens come from
2593
+ * `applySettingsOverrides`, the columns' backgrounds from the view cards, the
2594
+ * `--dsh-any-op-*` alphas from the same callback as the ink), so a palette update
2595
+ * that is deferred and then lost leaves the surfaces on the NEW colour and the
2596
+ * ink on the previous one — a dark palette with dark text, which reads as
2597
+ * "the theme colour did nothing" and only a reload clears it.
2598
+ *
2599
+ * Measured in the field: a window that is not rendering can swallow the frame
2600
+ * (the id stays armed, so every later update parks behind it), and a failure
2601
+ * inside the write used to be swallowed by an empty `catch`. Both are handled
2602
+ * here — a changed palette goes through synchronously, a stale frame is dropped
2603
+ * instead of blocking forever, and the failure is logged (see `applyCustomTokensNow`).
2604
+ */
2326
2605
  function applyCustomTokens(ops) {
2327
2606
  pendingOps = ops;
2328
- if (tokensRaf !== null) return;
2607
+ const src = palette();
2608
+ if (src !== null && paletteKey(src, rColor()) !== baseTokenKey) {
2609
+ flushTokens();
2610
+ return lastFadeable;
2611
+ }
2612
+ if (tokensRaf !== null) {
2613
+ if (Date.now() - tokensArmedAt <= STALE_TOKENS_RAF_MS) return false;
2614
+ window.cancelAnimationFrame(tokensRaf);
2615
+ tokensRaf = null;
2616
+ }
2617
+ tokensArmedAt = Date.now();
2329
2618
  tokensRaf = requestAnimationFrame(() => {
2330
2619
  tokensRaf = null;
2331
- if (pendingOps === null) return;
2332
- const o = pendingOps;
2333
- pendingOps = null;
2334
- applyCustomTokensNow(o);
2620
+ flushTokens();
2335
2621
  });
2622
+ return false;
2623
+ }
2624
+ /** Run the pending token write now (one funnel, so no path can skip it). */
2625
+ function flushTokens() {
2626
+ if (pendingOps === null) return;
2627
+ const o = pendingOps;
2628
+ pendingOps = null;
2629
+ applyCustomTokensNow(o);
2336
2630
  }
2337
2631
  let lastBgKey = "";
2632
+ /**
2633
+ * Whether the palette write in flight may fade — the answer `applyCustomTokens`
2634
+ * hands back to the callers that paint their own surfaces inline.
2635
+ *
2636
+ * It has to travel out of `applyCustomTokensNow` (which is where the decision is
2637
+ * made) because those callers run AFTER it in the same task, and re-deciding
2638
+ * there would be a second opinion on the same question.
2639
+ */
2640
+ let lastFadeable = false;
2338
2641
  function applyCustomTokensNow(ops) {
2642
+ lastFadeable = false;
2339
2643
  const src = palette();
2340
2644
  if (src === null) {
2341
2645
  clearCustomTokens();
@@ -2347,8 +2651,18 @@ window.__ModuleLoader__.load({
2347
2651
  const [h, s, l] = rColor();
2348
2652
  try {
2349
2653
  const forceDark = own && l < .55;
2350
- const key = own ? `own:${h}|${s}|${l}` : `host:${HOST_SURFACE_TOKENS.map((n) => tokens[n] ?? "").join("|")}`;
2351
- if (key !== baseTokenKey) {
2654
+ const key = paletteKey(src, [
2655
+ h,
2656
+ s,
2657
+ l
2658
+ ]);
2659
+ const paletteChanged = key !== baseTokenKey;
2660
+ const scheme = own ? forceDark ? "dark" : "light" : resolveHostDark() ? "dark" : "light";
2661
+ const fadeable = paletteChanged && paletteFadeAllowed(paletteScheme, scheme);
2662
+ lastFadeable = fadeable;
2663
+ applyPaletteFade(fadeable);
2664
+ if (paletteChanged) {
2665
+ paletteScheme = scheme;
2352
2666
  if (own) {
2353
2667
  if (forceDark) document.body.setAttribute(DARK_ATTR, OWN_SCHEME);
2354
2668
  else document.body.removeAttribute(DARK_ATTR);
@@ -2371,9 +2685,11 @@ window.__ModuleLoader__.load({
2371
2685
  const bgKey = `${baseTokenKey}|${ops.bg}`;
2372
2686
  if (bgKey !== lastBgKey) {
2373
2687
  lastBgKey = bgKey;
2374
- applyPartOpacities(ops);
2688
+ applyPartOpacities(ops, fadeable);
2375
2689
  }
2376
- } catch {}
2690
+ } catch (e) {
2691
+ console.warn("dsh-background-by-model: could not re-emit the palette tokens", e);
2692
+ }
2377
2693
  }
2378
2694
  const SETTINGS_PANEL_SEL = "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]";
2379
2695
  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)}`;
@@ -2408,6 +2724,100 @@ window.__ModuleLoader__.load({
2408
2724
  write("--dsh-any-traj-layer-2", "--dsw-alias-bg-layer-2");
2409
2725
  write("--dsh-any-traj-layer-3", "--dsw-alias-bg-layer-3");
2410
2726
  }
2727
+ /** Elements whose own token declarations change when the palette does: the body
2728
+ * (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]"
2734
+ ];
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`). */
2738
+ function fadeTokenNames() {
2739
+ return Object.keys(genTokens(220, .55, .25).tokens);
2740
+ }
2741
+ let fadeStyleEl = null;
2742
+ let fadeInitials = null;
2743
+ let fadeCssCache = "";
2744
+ /** The scheme the last written palette had, so a FLIP can be told from a tint. */
2745
+ let paletteScheme = null;
2746
+ function ensureFadeSheet() {
2747
+ if (fadeStyleEl?.isConnected) return fadeStyleEl;
2748
+ fadeStyleEl = document.createElement("style");
2749
+ fadeStyleEl.dataset.plugin = "dsh-background-by-model-fade";
2750
+ markOwnSheet(fadeStyleEl);
2751
+ document.head.appendChild(fadeStyleEl);
2752
+ return fadeStyleEl;
2753
+ }
2754
+ /**
2755
+ * The host's own value for each token — the `initial-value` a registration needs,
2756
+ * and the test for whether a token may be registered at all.
2757
+ *
2758
+ * Read with the plugin's token sheet muted, for the same reason `readHostPalette`
2759
+ * does it: our rule re-emits the very names being asked for. Cached, because the
2760
+ * answer only changes with the host's scheme and a stale hint is only ever used
2761
+ * for a token the host does not publish anyway.
2762
+ */
2763
+ function fadeInitialsOf(names) {
2764
+ if (fadeInitials !== null) return fadeInitials;
2765
+ const sheet = tokenStyleEl;
2766
+ const muted = sheet !== null && sheet.isConnected && !sheet.disabled;
2767
+ if (muted) sheet.disabled = true;
2768
+ try {
2769
+ const computed = getComputedStyle(document.body);
2770
+ const out = {};
2771
+ for (const name of names) {
2772
+ const used = resolveCssColor(computed.getPropertyValue(name));
2773
+ if (used !== null) out[name] = used;
2774
+ }
2775
+ fadeInitials = out;
2776
+ return out;
2777
+ } catch {
2778
+ fadeInitials = {};
2779
+ return fadeInitials;
2780
+ } finally {
2781
+ if (muted) sheet.disabled = false;
2782
+ }
2783
+ }
2784
+ /** How long a colour change may take, or `0` for "do not animate it".
2785
+ *
2786
+ * The same decision the wallpaper layer makes (`transitionPlan` over the global
2787
+ * effect, the resolved duration and the OS preference), so the two halves of one
2788
+ * switch can never disagree about whether they are animating. */
2789
+ function paletteFadeMs() {
2790
+ const tr = rTransition();
2791
+ const plan = transitionPlan({
2792
+ effect: tr.effect,
2793
+ easing: tr.easing,
2794
+ durationMs: rFadeMs(),
2795
+ canAnimate: true,
2796
+ reducedMotion: prefersReducedMotion()
2797
+ });
2798
+ return plan.animate ? plan.durationMs : 0;
2799
+ }
2800
+ /** Arm the fade for a palette change, or disarm it for anything else.
2801
+ *
2802
+ * `animate` is false on every pass that only moves an alpha (a slider drag): the
2803
+ * tokens are rewritten there too, and a live transition would make the surface
2804
+ * lag behind the pointer. */
2805
+ function applyPaletteFade(animate) {
2806
+ const initials = fadeInitialsOf(fadeTokenNames());
2807
+ const css = paletteFadeCss({
2808
+ selectors: FADE_SELECTORS,
2809
+ initial: initials,
2810
+ durationMs: animate ? paletteFadeMs() : 0,
2811
+ easing: rTransition().easing
2812
+ });
2813
+ if (css === fadeCssCache) return;
2814
+ fadeCssCache = css;
2815
+ ensureFadeSheet().textContent = css;
2816
+ }
2817
+ /** Inline `transition` for an element the plugin paints itself; `0` disarms it. */
2818
+ function fadeInlineFor(animate) {
2819
+ return paletteFadeInline(animate ? paletteFadeMs() : 0, rTransition().easing);
2820
+ }
2411
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)}";
2412
2822
  /** Paint the file-preview panel's surface from its own slider. */
2413
2823
  function applyRightbarOverrides(op) {
@@ -2485,7 +2895,7 @@ window.__ModuleLoader__.load({
2485
2895
  * the frame. The frame's translucent bg-base sits UNDER the sidebar, so
2486
2896
  * reducing the main-bg opacity stacked a second alpha onto the sidebar; moving
2487
2897
  * the alpha onto the columns keeps the sidebar owned by its own slider. */
2488
- function applyPartOpacities(ops) {
2898
+ function applyPartOpacities(ops, fade = false) {
2489
2899
  discoverParts();
2490
2900
  if (frameEl === null) return;
2491
2901
  const base = palette()?.tokens["--dsw-alias-bg-base"];
@@ -2498,8 +2908,15 @@ window.__ModuleLoader__.load({
2498
2908
  return;
2499
2909
  }
2500
2910
  frameEl.style.background = "transparent";
2501
- if (centerEl !== null) centerEl.style.background = withAlpha(base, ops.bg);
2502
- if (detailsEl !== null) detailsEl.style.background = withAlpha(base, ops.bg);
2911
+ const transition = fadeInlineFor(fade);
2912
+ if (centerEl !== null) {
2913
+ centerEl.style.transition = transition;
2914
+ centerEl.style.background = withAlpha(base, ops.bg);
2915
+ }
2916
+ if (detailsEl !== null) {
2917
+ detailsEl.style.transition = transition;
2918
+ detailsEl.style.background = withAlpha(base, ops.bg);
2919
+ }
2503
2920
  }
2504
2921
  /** Blur of the option panels inside the settings dialog (.dab-card), owned by
2505
2922
  * the "dialog option panel" (card) blur slider. Written as a plugin-owned
@@ -2709,10 +3126,11 @@ window.__ModuleLoader__.load({
2709
3126
  if (!tableFixStyleEl.isConnected) document.head.appendChild(tableFixStyleEl);
2710
3127
  }
2711
3128
  /** Re-derive the conversation view cards from the current config. */
2712
- function applyViewCards() {
3129
+ function applyViewCards(fade = false) {
2713
3130
  discoverParts();
2714
3131
  if (centerEl === null) return;
2715
3132
  const surface = palette()?.tokens["--dsw-alias-bg-layer-1"];
3133
+ const transition = fadeInlineFor(fade);
2716
3134
  VIEW_CARDS.forEach((spec, i) => {
2717
3135
  const target = discoverViewTarget(i, spec);
2718
3136
  if (target === null) return;
@@ -2722,6 +3140,7 @@ window.__ModuleLoader__.load({
2722
3140
  if (!plain) {
2723
3141
  if (!target.hasAttribute(spec.mark)) stashCardPrev(target, spec.prev, false);
2724
3142
  const borderAlpha = opacity > 0 ? Math.min(1, opacity * 1.5) : blurPx > 0 ? .35 : 0;
3143
+ target.style.transition = transition;
2725
3144
  target.style.background = surface !== void 0 ? withAlpha(surface, opacity) : "transparent";
2726
3145
  target.style.border = surface !== void 0 ? `1px solid ${withAlpha(surface, borderAlpha)}` : "1px solid transparent";
2727
3146
  target.style.borderRadius = "16px";
@@ -2844,6 +3263,13 @@ window.__ModuleLoader__.load({
2844
3263
  l.el.style.backgroundImage = "";
2845
3264
  l.el.style.backgroundSize = "";
2846
3265
  l.el.style.backgroundPosition = "";
3266
+ l.el.style.transform = "none";
3267
+ }
3268
+ /** Write the two properties a switch animates. Both are always written, so a
3269
+ * layer can never keep half of a previous effect. */
3270
+ function applyLayerStyle(el, style) {
3271
+ el.style.opacity = style.opacity;
3272
+ el.style.transform = style.transform;
2847
3273
  }
2848
3274
  /** Intrinsic-size cache for the center mode (native pixels of the current image). */
2849
3275
  let imgNat = null;
@@ -2912,13 +3338,18 @@ window.__ModuleLoader__.load({
2912
3338
  });
2913
3339
  }
2914
3340
  }
2915
- /** Whether a switch should animate: nothing to fade from, an invisible
2916
- * wallpaper and a reduced-motion preference all switch instantly. */
2917
- function canFade(from) {
3341
+ /** Whether there is anything to animate FROM: a wallpaper on screen that is
3342
+ * actually visible. The duration question belongs to `rFadeMs`, and the
3343
+ * `reduced-motion` veto to `transitionPlan` — this answers only the layer half,
3344
+ * so a first paint (no previous image) never fades in from nothing. */
3345
+ function canAnimateFrom(from) {
2918
3346
  if (from.url === null) return false;
2919
- if (rWop() <= .01) return false;
3347
+ return rWop() > .01;
3348
+ }
3349
+ /** Whether the OS asks for reduced motion right now. */
3350
+ function prefersReducedMotion() {
2920
3351
  const mm = typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
2921
- return mm === null || !mm.matches;
3352
+ return mm !== null && mm.matches;
2922
3353
  }
2923
3354
  /** End an in-flight fade: promote the incoming layer, free the one under it. */
2924
3355
  function finishFade(idx) {
@@ -2931,6 +3362,7 @@ window.__ModuleLoader__.load({
2931
3362
  front = idx;
2932
3363
  const el = layers[idx].el;
2933
3364
  el.style.opacity = "1";
3365
+ el.style.transform = "none";
2934
3366
  el.style.willChange = "";
2935
3367
  clearLayer(layers[idx === 0 ? 1 : 0]);
2936
3368
  }
@@ -2947,6 +3379,7 @@ window.__ModuleLoader__.load({
2947
3379
  const el = layers[idx].el;
2948
3380
  el.style.transition = "none";
2949
3381
  el.style.opacity = "1";
3382
+ el.style.transform = "none";
2950
3383
  el.style.willChange = "";
2951
3384
  clearLayer(layers[idx === 0 ? 1 : 0]);
2952
3385
  }
@@ -2962,6 +3395,7 @@ window.__ModuleLoader__.load({
2962
3395
  const el = layers[idx].el;
2963
3396
  el.style.transition = "none";
2964
3397
  el.style.opacity = "0";
3398
+ el.style.transform = "none";
2965
3399
  el.style.willChange = "";
2966
3400
  clearLayer(layers[idx]);
2967
3401
  }
@@ -2992,27 +3426,34 @@ window.__ModuleLoader__.load({
2992
3426
  const idx = front === 0 ? 1 : 0;
2993
3427
  const target = ls[idx];
2994
3428
  const el = target.el;
3429
+ const tr = rTransition();
3430
+ const plan = transitionPlan({
3431
+ effect: tr.effect,
3432
+ easing: tr.easing,
3433
+ durationMs: rFadeMs(),
3434
+ canAnimate: canAnimateFrom(from),
3435
+ reducedMotion: prefersReducedMotion()
3436
+ });
2995
3437
  el.style.transition = "none";
2996
- el.style.opacity = "0";
3438
+ el.style.willChange = plan.willChange;
3439
+ applyLayerStyle(el, plan.from);
2997
3440
  el.style.zIndex = "1";
2998
3441
  from.el.style.zIndex = "0";
2999
3442
  paintLayer(target, url);
3000
3443
  applyWpEffects();
3001
- const fadeMs = rFadeMs();
3002
- if (!canFade(from) || fadeMs <= 0) {
3003
- el.style.opacity = "1";
3444
+ if (!plan.animate) {
3445
+ applyLayerStyle(el, plan.to);
3004
3446
  front = idx;
3005
3447
  clearLayer(from);
3006
3448
  return;
3007
3449
  }
3008
- el.style.willChange = "opacity";
3009
3450
  pending = idx;
3010
3451
  requestAnimationFrame(() => {
3011
3452
  if (pending !== idx || layers === null) return;
3012
- el.style.transition = `opacity ${fadeMs}ms ease`;
3013
- el.style.opacity = "1";
3453
+ el.style.transition = plan.transition;
3454
+ applyLayerStyle(el, plan.to);
3014
3455
  });
3015
- fadeTimer = window.setTimeout(() => finishFade(idx), fadeMs + 160);
3456
+ fadeTimer = window.setTimeout(() => finishFade(idx), plan.durationMs + 160);
3016
3457
  }
3017
3458
  function applyWpEffects() {
3018
3459
  if (wpEl === null || layers === null) return;
@@ -3039,11 +3480,12 @@ window.__ModuleLoader__.load({
3039
3480
  clearCustomTokens();
3040
3481
  clearOwnedSurfaces();
3041
3482
  }
3042
- applyCustomTokens(rOps());
3483
+ const paletteMoved = applyCustomTokens(rOps());
3043
3484
  applySettingsOverrides(rSop());
3044
3485
  applyTrajectoryOverrides(rTrajectoryOpacity());
3045
3486
  applyRightbarOverrides(rRightbarOpacity());
3046
3487
  applyPartBlurs(rBlurs());
3488
+ applyViewCards(paletteMoved);
3047
3489
  }
3048
3490
  function teardownWp() {
3049
3491
  dropWpContainer();
@@ -5637,11 +6079,58 @@ window.__ModuleLoader__.load({
5637
6079
  }
5638
6080
  //#endregion
5639
6081
  //#region src/client/components/pages/ProfilePage.tsx
6082
+ /** Chip labels, keyed by the value they set — so no chip can be built from a
6083
+ * value the schema does not accept, and `check:ui` still sees every key used. */
6084
+ const EFFECT_KEYS = {
6085
+ fade: "trEffectFade",
6086
+ none: "trEffectNone",
6087
+ zoom: "trEffectZoom",
6088
+ slide: "trEffectSlide"
6089
+ };
6090
+ const EASING_KEYS = {
6091
+ ease: "trEasingEase",
6092
+ linear: "trEasingLinear",
6093
+ "ease-out": "trEasingOut",
6094
+ "ease-in-out": "trEasingInOut"
6095
+ };
5640
6096
  function ProfilePage({ p, notify }) {
5641
- const { t, exportTheme, importTheme } = p;
6097
+ const { t, exportTheme, importTheme, setTransition } = p;
5642
6098
  const importRef = (0, react.useRef)(null);
5643
6099
  p.useStore((s) => s.rev + s.rulesRev);
5644
6100
  const holidays = cfg.holidays;
6101
+ const tr = cfg.transition;
6102
+ const previewRef = (0, react.useRef)(null);
6103
+ /** The OS preference, read at render: it vetoes the real switch (see
6104
+ * `transitionPlan`), so the card has to say so rather than show a preview of
6105
+ * something the interface will not do. */
6106
+ const reduced = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
6107
+ /**
6108
+ * Play the effect on the preview swatch, for the duration a real switch would
6109
+ * use right now: the wallpaper fading into ITSELF is invisible, which is the
6110
+ * one thing a transition setting must not leave the user guessing about.
6111
+ */
6112
+ const play = () => {
6113
+ const el = previewRef.current;
6114
+ if (el === null) return;
6115
+ const plan = transitionPlan({
6116
+ effect: tr.effect,
6117
+ easing: tr.easing,
6118
+ durationMs: rFadeMs(),
6119
+ canAnimate: true,
6120
+ reducedMotion: false
6121
+ });
6122
+ el.style.transition = "none";
6123
+ el.style.opacity = plan.from.opacity;
6124
+ el.style.transform = plan.from.transform;
6125
+ requestAnimationFrame(() => {
6126
+ el.style.transition = plan.transition;
6127
+ el.style.opacity = plan.to.opacity;
6128
+ el.style.transform = plan.to.transform;
6129
+ });
6130
+ };
6131
+ (0, react.useEffect)(() => {
6132
+ if (!reduced) play();
6133
+ }, [tr.effect, tr.easing]);
5645
6134
  const onImport = async (file) => {
5646
6135
  try {
5647
6136
  const ok = await importTheme(file);
@@ -5689,9 +6178,106 @@ window.__ModuleLoader__.load({
5689
6178
  })]
5690
6179
  })
5691
6180
  }),
6181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
6182
+ className: "dab-card dab-rise",
6183
+ style: { "--d": 1 },
6184
+ children: [
6185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6186
+ className: "dab-part-head",
6187
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6188
+ className: "dab-part-ico",
6189
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 16 })
6190
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6191
+ className: "dab-part-name",
6192
+ children: t("trTitle")
6193
+ })]
6194
+ }),
6195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6196
+ className: "dab-swatch-title",
6197
+ children: t("trEffect")
6198
+ }),
6199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6200
+ className: "dab-chip-row",
6201
+ children: TRANSITION_EFFECTS.map((effect) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6202
+ type: "button",
6203
+ className: `dab-chip${tr.effect === effect ? " is-active" : ""}`,
6204
+ onClick: () => setTransition({ effect }),
6205
+ children: t(EFFECT_KEYS[effect])
6206
+ }, effect))
6207
+ }),
6208
+ tr.effect === "none" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6209
+ className: "dab-swatch-title",
6210
+ style: { marginTop: 12 },
6211
+ children: t("trEasing")
6212
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6213
+ className: "dab-chip-row",
6214
+ children: TRANSITION_EASINGS.map((easing) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6215
+ type: "button",
6216
+ className: `dab-chip${tr.easing === easing ? " is-active" : ""}`,
6217
+ onClick: () => setTransition({ easing }),
6218
+ children: t(EASING_KEYS[easing])
6219
+ }, easing))
6220
+ })] }),
6221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6222
+ className: "dab-swatch-title",
6223
+ style: { marginTop: 12 },
6224
+ children: t("trDuration")
6225
+ }),
6226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6227
+ style: { marginTop: 2 },
6228
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
6229
+ label: t("trDuration"),
6230
+ min: 0,
6231
+ max: 3e3,
6232
+ step: 50,
6233
+ def: tr.durationMs,
6234
+ fmt: (v) => `${v} ms`,
6235
+ onInput: (v) => {
6236
+ cfg.transition = normalizeTransition({
6237
+ ...cfg.transition,
6238
+ durationMs: v
6239
+ });
6240
+ saveConfig();
6241
+ },
6242
+ onChange: (v) => setTransition({ durationMs: v })
6243
+ })
6244
+ }),
6245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6246
+ style: {
6247
+ display: "flex",
6248
+ alignItems: "center",
6249
+ gap: 12,
6250
+ marginTop: 13
6251
+ },
6252
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6253
+ className: "dab-tr-preview",
6254
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6255
+ ref: previewRef,
6256
+ className: "dab-tr-preview-in"
6257
+ })
6258
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6259
+ type: "button",
6260
+ className: "dab-btn",
6261
+ disabled: reduced,
6262
+ onClick: play,
6263
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 }), t("trPreview")]
6264
+ })]
6265
+ }),
6266
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6267
+ className: "dab-hint",
6268
+ style: { marginTop: 10 },
6269
+ children: t("trHint")
6270
+ }),
6271
+ reduced ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6272
+ className: "dab-hint",
6273
+ style: { marginTop: 6 },
6274
+ children: t("trReducedHint")
6275
+ }) : null
6276
+ ]
6277
+ }),
5692
6278
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5693
6279
  className: "dab-profile-grid dab-rise",
5694
- style: { "--d": 2 },
6280
+ style: { "--d": 3 },
5695
6281
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5696
6282
  className: "dab-card dab-card-hover",
5697
6283
  children: [
@@ -5755,7 +6341,7 @@ window.__ModuleLoader__.load({
5755
6341
  }),
5756
6342
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
5757
6343
  className: "dab-footer dab-rise",
5758
- style: { "--d": 3 },
6344
+ style: { "--d": 4 },
5759
6345
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5760
6346
  className: "dab-footer-mono",
5761
6347
  children: "dsh-background-by-model"
@@ -6642,6 +7228,24 @@ window.__ModuleLoader__.load({
6642
7228
  saveConfig();
6643
7229
  sync();
6644
7230
  },
7231
+ /**
7232
+ * Patch the GLOBAL wallpaper-switch transition (effect, easing, where the
7233
+ * duration comes from).
7234
+ *
7235
+ * Deliberately no `applyWp()`: this setting decides what the NEXT switch
7236
+ * looks like and nothing on screen changes right now, so re-running the
7237
+ * whole apply (token rewrite, every interface part) for something that
7238
+ * paints nothing would be the expensive way to say "saved". The Config
7239
+ * page's own preview plays the effect instead.
7240
+ */
7241
+ setTransition: (patch) => {
7242
+ cfg.transition = normalizeTransition({
7243
+ ...cfg.transition,
7244
+ ...patch
7245
+ });
7246
+ sync();
7247
+ saveConfig();
7248
+ },
6645
7249
  setHolidaysEnabled: (v) => {
6646
7250
  cfg.holidays.enabled = v;
6647
7251
  persistConfig();
@@ -6657,7 +7261,7 @@ window.__ModuleLoader__.load({
6657
7261
  if (url !== null) images[slot] = url;
6658
7262
  }
6659
7263
  const payload = {
6660
- version: 5,
7264
+ version: 6,
6661
7265
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
6662
7266
  config: cfg,
6663
7267
  images