dsh-any-background 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -12,9 +12,15 @@ window.__ModuleLoader__.load({
12
12
  try {
13
13
  return require("@deepseek-ai/dsh-client-store");
14
14
  } catch {}
15
- return require("@deepseek-ai/dsh-client-runtime/client");
15
+ try {
16
+ return require("@deepseek-ai/dsh-client-runtime/client");
17
+ } catch {
18
+ console.warn("dsh-any-background: no client store module resolved on this host; the settings panel will be disabled");
19
+ return null;
20
+ }
16
21
  }
17
- const defineStore = resolveStore().defineStore;
22
+ /** null when neither store package resolves on this host (see resolveStore). */
23
+ const defineStore = resolveStore()?.defineStore ?? null;
18
24
  //#endregion
19
25
  //#region src/client/i18n.ts
20
26
  const NS = "settings.anyBg";
@@ -46,8 +52,11 @@ window.__ModuleLoader__.load({
46
52
  uiSop: "设置界面透明度",
47
53
  uiChatRegion: "对话文本框",
48
54
  uiTrajectory: "轨迹页",
49
- uiPanelRegion: "better-sidebar 工作台",
55
+ uiPanelRegion: "bettersidebar",
56
+ uiPanelNative: "右方侧边栏",
57
+ guideDesc: "管理主题色、壁纸与界面透明度",
50
58
  uiProduced: "产出物/高亮内容",
59
+ uiHeader: "顶栏选项",
51
60
  fontPageTitle: "字体与描边",
52
61
  fontTitle: "自定义字体",
53
62
  fontHint: "支持 ttf/otf/woff/woff2 字体文件(上限 100MB),将应用到整个界面的文字;代码块的等宽字体保持不变。中文字体通常有几十 MB,首次上传与刷新加载需要片刻",
@@ -57,6 +66,9 @@ window.__ModuleLoader__.load({
57
66
  fontEnabled: "启用字体",
58
67
  fontNone: "未设置,使用宿主默认字体",
59
68
  fontFail: "字体上传失败,请确认是有效的 ttf/otf/woff/woff2 文件",
69
+ uploadTooLarge: "文件过大,上限 {limit}",
70
+ bgUploadFail: "壁纸上传失败",
71
+ videoUploadFail: "视频上传失败,本次播放不会被保存",
60
72
  strokeTitle: "文字描边",
61
73
  strokeHint: "按区域为文字添加描边:粗细为 0 表示关闭,「自动」会生成与文字颜色相反的颜色,「主题色」跟随当前主题色。代码块与图标已自动豁免,多色语法不会糊成一团",
62
74
  strokeWidth: "描边粗细",
@@ -232,8 +244,11 @@ window.__ModuleLoader__.load({
232
244
  uiSop: "Settings interface opacity",
233
245
  uiChatRegion: "Conversation text frame",
234
246
  uiTrajectory: "Trajectory view",
235
- uiPanelRegion: "better-sidebar workbench",
247
+ uiPanelRegion: "bettersidebar",
248
+ uiPanelNative: "Right sidebar",
249
+ guideDesc: "Theme color, wallpaper and interface opacity in the right sidebar",
236
250
  uiProduced: "Produced / highlights",
251
+ uiHeader: "Header popovers",
237
252
  fontPageTitle: "Font & outline",
238
253
  fontTitle: "Custom font",
239
254
  fontHint: "Accepts ttf/otf/woff/woff2 files (up to 100MB) and applies them to the whole interface; code blocks keep their monospace stack. CJK fonts routinely span tens of MB, so the first upload and each reload take a moment",
@@ -243,6 +258,9 @@ window.__ModuleLoader__.load({
243
258
  fontEnabled: "Font enabled",
244
259
  fontNone: "None — host default",
245
260
  fontFail: "Upload failed — make sure it is a valid ttf/otf/woff/woff2 file",
261
+ uploadTooLarge: "File is too large (limit {limit})",
262
+ bgUploadFail: "Wallpaper upload failed",
263
+ videoUploadFail: "Video upload failed — this playback will not be saved",
246
264
  strokeTitle: "Text outline",
247
265
  strokeHint: "Outline text per region: width 0 turns it off, \"auto\" picks the inverse of the text color, and \"theme\" follows the current accent. Code blocks and icons are exempted automatically, so multi-color syntax never smears",
248
266
  strokeWidth: "Outline width",
@@ -409,7 +427,8 @@ window.__ModuleLoader__.load({
409
427
  trajectory: 30,
410
428
  input: 30,
411
429
  panel: 30,
412
- produced: 30
430
+ produced: 30,
431
+ header: 30
413
432
  },
414
433
  strokes: {
415
434
  bg: {
@@ -456,6 +475,11 @@ window.__ModuleLoader__.load({
456
475
  width: 0,
457
476
  color: "auto",
458
477
  customColor: "#808080"
478
+ },
479
+ header: {
480
+ width: 0,
481
+ color: "auto",
482
+ customColor: "#808080"
459
483
  }
460
484
  },
461
485
  settingsOpacity: .5,
@@ -486,6 +510,7 @@ window.__ModuleLoader__.load({
486
510
  trajectoryOpacity: .5,
487
511
  panelOpacity: .5,
488
512
  producedOpacity: .5,
513
+ headerOpacity: .5,
489
514
  profiles: [],
490
515
  rotation: {
491
516
  enabled: false,
@@ -625,7 +650,8 @@ window.__ModuleLoader__.load({
625
650
  "trajectory",
626
651
  "input",
627
652
  "panel",
628
- "produced"
653
+ "produced",
654
+ "header"
629
655
  ]) {
630
656
  const v = b[k];
631
657
  out[k] = typeof v === "number" ? Math.min(60, Math.max(0, v)) : DEFAULT_CONFIG.blurs[k];
@@ -641,7 +667,8 @@ window.__ModuleLoader__.load({
641
667
  "trajectory",
642
668
  "input",
643
669
  "panel",
644
- "produced"
670
+ "produced",
671
+ "header"
645
672
  ];
646
673
  const STROKE_COLOR_KEYS = [
647
674
  "auto",
@@ -690,6 +717,9 @@ window.__ModuleLoader__.load({
690
717
  function rProducedOpacity() {
691
718
  return clamp01(cfg.producedOpacity, DEFAULT_CONFIG.producedOpacity);
692
719
  }
720
+ function rHeaderOpacity() {
721
+ return clamp01(cfg.headerOpacity, DEFAULT_CONFIG.headerOpacity);
722
+ }
693
723
  function rBgState() {
694
724
  return cfg.bgState;
695
725
  }
@@ -753,7 +783,8 @@ window.__ModuleLoader__.load({
753
783
  chatTextOpacity: rChatTextOpacity(),
754
784
  trajectoryOpacity: rTrajectoryOpacity(),
755
785
  panelOpacity: rPanelOpacity(),
756
- producedOpacity: rProducedOpacity()
786
+ producedOpacity: rProducedOpacity(),
787
+ headerOpacity: rHeaderOpacity()
757
788
  };
758
789
  }
759
790
  /** Apply an appearance snapshot onto cfg (meta fields untouched). */
@@ -775,6 +806,7 @@ window.__ModuleLoader__.load({
775
806
  cfg.trajectoryOpacity = clamp01(ap.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity);
776
807
  cfg.panelOpacity = clamp01(ap.panelOpacity, DEFAULT_CONFIG.panelOpacity);
777
808
  cfg.producedOpacity = clamp01(ap.producedOpacity, DEFAULT_CONFIG.producedOpacity);
809
+ cfg.headerOpacity = clamp01(ap.headerOpacity, DEFAULT_CONFIG.headerOpacity);
778
810
  }
779
811
  const num = (n, def) => typeof n === "number" ? n : def;
780
812
  const cl = (n, lo, hi, def) => typeof n === "number" ? Math.min(hi, Math.max(lo, n)) : def;
@@ -808,7 +840,8 @@ window.__ModuleLoader__.load({
808
840
  chatTextOpacity: clamp01(ac.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
809
841
  trajectoryOpacity: clamp01(ac.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity),
810
842
  panelOpacity: clamp01(ac.panelOpacity, DEFAULT_CONFIG.panelOpacity),
811
- producedOpacity: clamp01(ac.producedOpacity, DEFAULT_CONFIG.producedOpacity)
843
+ producedOpacity: clamp01(ac.producedOpacity, DEFAULT_CONFIG.producedOpacity),
844
+ headerOpacity: clamp01(ac.headerOpacity, DEFAULT_CONFIG.headerOpacity)
812
845
  }
813
846
  });
814
847
  }
@@ -873,7 +906,8 @@ window.__ModuleLoader__.load({
873
906
  "trajectory",
874
907
  "input",
875
908
  "panel",
876
- "produced"
909
+ "produced",
910
+ "header"
877
911
  ]) blurs[k] = num(bl[k], DEFAULT_CONFIG.blurs[k]);
878
912
  const bgType = [
879
913
  "video",
@@ -916,6 +950,7 @@ window.__ModuleLoader__.load({
916
950
  trajectoryOpacity: clamp01(c.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity),
917
951
  panelOpacity: clamp01(c.panelOpacity, DEFAULT_CONFIG.panelOpacity),
918
952
  producedOpacity: clamp01(c.producedOpacity, DEFAULT_CONFIG.producedOpacity),
953
+ headerOpacity: clamp01(c.headerOpacity, DEFAULT_CONFIG.headerOpacity),
919
954
  profiles: adoptProfiles(c.profiles),
920
955
  rotation: adoptRotation(c.rotation),
921
956
  schedule: adoptSchedule(c.schedule),
@@ -959,6 +994,95 @@ window.__ModuleLoader__.load({
959
994
  };
960
995
  }
961
996
  //#endregion
997
+ //#region src/client/host.ts
998
+ /**
999
+ * Host release detection + feature classification.
1000
+ *
1001
+ * WHY THIS EXISTS
1002
+ * The client context exposes no host version: there is no `hostVersion` /
1003
+ * `dshVersion` field on `ctx`, and `window.__DSH_BOOT__.version` is the
1004
+ * module-table format tag (`'client'`), not a release. Capability probing alone
1005
+ * is also not sufficient, because the obvious probes do not separate the
1006
+ * generations — `ctx.sidebarRightTabs` and the native `[data-sidebar-right-panel]`
1007
+ * marker both exist on 0.1.5-rc.2 already, so using either as a "0.1.6+" test
1008
+ * mis-classifies 0.1.5 as newer.
1009
+ *
1010
+ * So the release is resolved on the Node half (where the launcher's on-disk
1011
+ * layout is visible) and handed to this half through the `read` RPC payload.
1012
+ * This module stores that verdict and turns it into the feature flags the rest
1013
+ * of the plugin gates on. When the release cannot be determined the flags fall
1014
+ * back to capability probing, never to a guess.
1015
+ *
1016
+ * @module
1017
+ */
1018
+ let current = {
1019
+ version: null,
1020
+ generation: "unknown"
1021
+ };
1022
+ /** False until the server verdict lands; feature gates must not treat "not
1023
+ * asked yet" as "old host". */
1024
+ let resolved = false;
1025
+ const subs$1 = /* @__PURE__ */ new Set();
1026
+ /** Adopt the verdict delivered by the server's `read` payload. */
1027
+ function adoptHostInfo(raw) {
1028
+ if (raw === null || typeof raw !== "object") return;
1029
+ const h = raw;
1030
+ const version = typeof h.version === "string" && h.version.trim() !== "" ? h.version.trim() : null;
1031
+ const generation = h.generation === "0.1.5" || h.generation === "0.1.6" || h.generation === "0.1.7" ? h.generation : "unknown";
1032
+ const changed = !resolved || current.version !== version || current.generation !== generation;
1033
+ current = {
1034
+ version,
1035
+ generation
1036
+ };
1037
+ resolved = true;
1038
+ if (changed) subs$1.forEach((cb) => cb());
1039
+ }
1040
+ /** Subscribe to a verdict landing (or changing). */
1041
+ function subscribeHostInfo(cb) {
1042
+ subs$1.add(cb);
1043
+ return () => {
1044
+ subs$1.delete(cb);
1045
+ };
1046
+ }
1047
+ /** Ordered generation rank, for "at least this generation" tests. `unknown`
1048
+ * has no rank and must be handled by the caller's fallback. */
1049
+ const RANK = {
1050
+ "0.1.5": 5,
1051
+ "0.1.6": 6,
1052
+ "0.1.7": 7
1053
+ };
1054
+ /**
1055
+ * Whether the host is at least `gen`. An unknown generation answers `false`:
1056
+ * callers treat that as "do not assume the newer behaviour" and use whatever
1057
+ * capability probe they own, which is the safe direction for every gate here.
1058
+ */
1059
+ function hostAtLeast(gen) {
1060
+ const rank = RANK[current.generation];
1061
+ return rank !== void 0 && rank >= RANK[gen];
1062
+ }
1063
+ /**
1064
+ * Whether this host's own right Sidebar already carries the appearance page,
1065
+ * which makes the plugin's dsh-better-sidebar page a duplicate entry on the same
1066
+ * guide surface.
1067
+ *
1068
+ * `true` — 0.1.6 or newer: the better-sidebar page stands down.
1069
+ * `false` — 0.1.5 or an undetermined-but-answered generation: keep it.
1070
+ * `null` — the verdict has not landed yet (the probe is a Node round-trip).
1071
+ *
1072
+ * Callers must treat `null` as "not yet known": register nothing eagerly on a
1073
+ * `true`, and arm a withdrawal for a later `true` rather than blocking on it.
1074
+ *
1075
+ * NOTE: 0.1.5-rc.2 ALSO ships the native right Sidebar (verified: the
1076
+ * `ui-sidebar-right` entry is in that release's `dsh-web-app/cordis.patch.yml`),
1077
+ * so this gate deliberately keys off the release rather than off the presence of
1078
+ * the native sidebar service — the two are not the same fact.
1079
+ */
1080
+ function suppressesBetterSidebar() {
1081
+ if (!resolved) return null;
1082
+ if (current.generation === "unknown") return false;
1083
+ return hostAtLeast("0.1.6");
1084
+ }
1085
+ //#endregion
962
1086
  //#region src/client/rpc.ts
963
1087
  const RPC_CHANNEL = "/dsh-any-background";
964
1088
  /** Same-origin serve URL of the persisted video (enough for <video src>/fetch). */
@@ -1023,6 +1147,7 @@ window.__ModuleLoader__.load({
1023
1147
  const data = await rpcCall("read", {});
1024
1148
  if (data && typeof data === "object") {
1025
1149
  const d = data;
1150
+ adoptHostInfo(d.host);
1026
1151
  if (d.config) adoptConfig(d.config);
1027
1152
  if (typeof d.wallpaperUrl === "string") setWpImageUrl(d.wallpaperUrl);
1028
1153
  else if (d.wallpaperUrl === null) setWpImageUrl(null);
@@ -1117,36 +1242,73 @@ window.__ModuleLoader__.load({
1117
1242
  error: v?.error ?? "failed"
1118
1243
  };
1119
1244
  }
1245
+ /** Map a refused upload response onto an outcome. The body has to be read: the
1246
+ * node half names the reason there, and an oversized transfer answers
1247
+ * `413 { ok:false, error:'too large', limit:'100 MB' }`. Checking `res.ok`
1248
+ * alone threw that away, so an oversized upload surfaced as a bare failure —
1249
+ * or, on the wallpaper path, as nothing at all. */
1250
+ async function refusedUpload(res) {
1251
+ const body = await res.json().catch(() => null);
1252
+ if (body?.error === "too large") return {
1253
+ ok: false,
1254
+ refusal: {
1255
+ kind: "too-large",
1256
+ status: res.status,
1257
+ limit: typeof body.limit === "string" ? body.limit : void 0
1258
+ }
1259
+ };
1260
+ return {
1261
+ ok: false,
1262
+ refusal: {
1263
+ kind: "http",
1264
+ status: res.status
1265
+ }
1266
+ };
1267
+ }
1268
+ /** Localized description of a refusal for the panel's toast. `fallbackKey` is
1269
+ * the caller's own "…upload failed" string, used for anything that is not a
1270
+ * size refusal. */
1271
+ function uploadRefusalText(o, t, fallbackKey) {
1272
+ if (o.refusal?.kind === "too-large") {
1273
+ const limit = o.refusal.limit ?? "";
1274
+ return t("uploadTooLarge").split("{limit}").join(limit);
1275
+ }
1276
+ if (o.refusal !== void 0) return `${t(fallbackKey)} (http ${o.refusal.status})`;
1277
+ return t(fallbackKey);
1278
+ }
1120
1279
  /** Upload a video's raw bytes over HTTP (MIME in Content-Type, body untouched
1121
1280
  * — no base64 inflation that would blow the RPC body limit on large clips). */
1122
1281
  async function uploadVideo(blob, mime) {
1123
1282
  try {
1124
- return (await fetch(VIDEO_UPLOAD_URL, {
1283
+ const res = await fetch(VIDEO_UPLOAD_URL, {
1125
1284
  method: "POST",
1126
1285
  headers: { "Content-Type": mime || "application/octet-stream" },
1127
1286
  body: blob
1128
- })).ok;
1287
+ });
1288
+ return res.ok ? { ok: true } : await refusedUpload(res);
1129
1289
  } catch (e) {
1130
1290
  console.warn("dsh-any-background: video upload failed", e);
1131
- return false;
1291
+ return { ok: false };
1132
1292
  }
1133
1293
  }
1134
1294
  /** Upload a wallpaper's raw bytes over HTTP — same streaming model as videos,
1135
1295
  * original pixels preserved, zero base64 round-trips. */
1136
1296
  async function uploadWallpaper(blob) {
1137
1297
  try {
1138
- return (await fetch(WALLPAPER_UPLOAD_URL, {
1298
+ const res = await fetch(WALLPAPER_UPLOAD_URL, {
1139
1299
  method: "POST",
1140
1300
  headers: { "Content-Type": blob.type || "image/jpeg" },
1141
1301
  body: blob
1142
- })).ok;
1302
+ });
1303
+ return res.ok ? { ok: true } : await refusedUpload(res);
1143
1304
  } catch (e) {
1144
1305
  console.warn("dsh-any-background: wallpaper upload failed", e);
1145
- return false;
1306
+ return { ok: false };
1146
1307
  }
1147
1308
  }
1148
1309
  /** Upload a custom font's raw bytes over HTTP; resolves the sniffed MIME on
1149
- * success (the server validates the container from magic bytes) or null. */
1310
+ * success, or a refusal the caller can report (an oversized font used to be
1311
+ * indistinguishable from a corrupt one). */
1150
1312
  async function uploadFont(blob) {
1151
1313
  try {
1152
1314
  const res = await fetch(FONT_UPLOAD_URL, {
@@ -1154,12 +1316,15 @@ window.__ModuleLoader__.load({
1154
1316
  headers: { "Content-Type": blob.type || "application/octet-stream" },
1155
1317
  body: blob
1156
1318
  });
1157
- if (!res.ok) return null;
1319
+ if (!res.ok) return await refusedUpload(res);
1158
1320
  const data = await res.json().catch(() => null);
1159
- return typeof data?.mime === "string" ? data.mime : "font/ttf";
1321
+ return {
1322
+ ok: true,
1323
+ mime: typeof data?.mime === "string" ? data.mime : "font/ttf"
1324
+ };
1160
1325
  } catch (e) {
1161
1326
  console.warn("dsh-any-background: font upload failed", e);
1162
- return null;
1327
+ return { ok: false };
1163
1328
  }
1164
1329
  }
1165
1330
  /** Remove the stored custom font (server deletes every variant + clears the
@@ -1340,6 +1505,12 @@ window.__ModuleLoader__.load({
1340
1505
  v
1341
1506
  ];
1342
1507
  }
1508
+ /** Format an RGB triple as a lowercase hex string. Channels are rounded: some
1509
+ * callers feed float channels and a fractional byte would produce invalid
1510
+ * hex. */
1511
+ function rgbToHex(rgb) {
1512
+ return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
1513
+ }
1343
1514
  let tokensCacheKey = "";
1344
1515
  let tokensCache = null;
1345
1516
  /**
@@ -2483,9 +2654,15 @@ window.__ModuleLoader__.load({
2483
2654
  wpController = null;
2484
2655
  }
2485
2656
  /** Register a callback fired once a generated snapshot is ready (so the caller
2486
- * can re-sync the settings preview / store). */
2657
+ * can re-sync the settings preview / store). Returns an unsubscribe: HMR
2658
+ * re-runs apply and would otherwise leave the previous apply's closure as the
2659
+ * live listener (a stale this-session callback invoked by a lingering
2660
+ * controller). */
2487
2661
  function onGeneratedSnapshot(cb) {
2488
2662
  snapshotListener = cb;
2663
+ return () => {
2664
+ if (snapshotListener === cb) snapshotListener = null;
2665
+ };
2489
2666
  }
2490
2667
  function ensureTokenStyle() {
2491
2668
  if (tokenStyleEl?.isConnected) return tokenStyleEl;
@@ -2647,6 +2824,7 @@ window.__ModuleLoader__.load({
2647
2824
  lastBgKey = bgKey;
2648
2825
  applyPartOpacities(ops);
2649
2826
  }
2827
+ applyExemptDefaults();
2650
2828
  } catch {}
2651
2829
  }
2652
2830
  const SETTINGS_PANEL_SEL = "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]";
@@ -2665,17 +2843,98 @@ window.__ModuleLoader__.load({
2665
2843
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-part-blur-global", `blur(${px}px)`);
2666
2844
  else document.documentElement.style.removeProperty("--dsh-any-part-blur-global");
2667
2845
  }
2668
- const PANEL_TOKEN_RULE = "[data-dsh-bottom-panel],[data-sidebar-right-panel]{--dsw-alias-bg-base:var(--dsh-any-panel-bg-base);--dsw-alias-bg-layer-1:var(--dsh-any-panel-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-panel-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-panel-layer-3);position:fixed!important;z-index:26!important}[data-sidebar-right-panel][data-sidebar-right-panel=\"fullscreen\"]{z-index:40!important}";
2669
- const PANEL_BLUR_RULE = "[data-dsh-bottom-panel],[data-sidebar-right-panel]{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}";
2670
- const PRODUCED_RULE = "[data-code-block-content] pre,[data-code-block-banner],[data-composer-chip],:not(pre)>code{-webkit-backdrop-filter:var(--dsh-any-blur-prod,none);backdrop-filter:var(--dsh-any-blur-prod,none)}@supports (background:color-mix(in srgb,red 50%,transparent)){.md-code-block{background:transparent!important}.md-code-block>div{background-color:transparent!important}.md-code-block [data-code-block-content] pre{background-color:color-mix(in srgb,var(--dsl-code-block-background,var(--dsw-alias-markdown-code-block,transparent)) var(--dsh-any-prod-pct,100%),transparent)!important}.md-code-block [data-code-block-banner]{background-color:color-mix(in srgb,var(--dsl-code-block-banner-background-color,var(--dsw-alias-markdown-code-block-banner,transparent)) var(--dsh-any-prod-pct,100%),transparent)!important}[data-code-block-content]{--shiki-background:color-mix(in srgb,var(--dsw-alias-markdown-code-block,var(--dsl-code-block-background,transparent)) var(--dsh-any-prod-pct,100%),transparent)}:not(pre)>code{background-color:color-mix(in srgb,var(--dsw-alias-markdown-inline-code,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-composer-chip]>*{background-color:color-mix(in srgb,var(--dsw-alias-interactive-bg-hover,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}}";
2846
+ const PANEL_TOKEN_RULE = "[data-dsh-bottom-panel],[data-sidebar-right-panel]{--dsw-alias-bg-base:var(--dsh-any-panel-bg-base);--dsw-alias-bg-layer-1:var(--dsh-any-panel-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-panel-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-panel-layer-3)}";
2847
+ const NO_DOCKKIT = ":not(:has([data-dockkit-host],[data-dockkit-empty]))";
2848
+ const PANEL_BLUR_RULE = `[data-sidebar-right-panel] [data-dockkit-host="dock"],[data-sidebar-right-panel] [data-dockkit-empty]{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}[data-sidebar-right-panel]${NO_DOCKKIT}{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}[data-dsh-bottom-panel]{-webkit-backdrop-filter:var(--dsh-any-blur-panel,none);backdrop-filter:var(--dsh-any-blur-panel,none)}`;
2849
+ const PANEL_PROMOTION_RULE = `[data-sidebar-right-panel]${NO_DOCKKIT}{position:fixed!important;z-index:26!important}[data-sidebar-right-panel="fullscreen"]${NO_DOCKKIT}{z-index:40!important}`;
2850
+ const HEADER_POPOVER_RULE = "[data-dsh-any-header-popover][role],ul[class*=\"_menu\"],div[role=\"dialog\"][class*=\"_panel\"]:not([aria-modal=\"true\"]),[role=\"menu\"][class*=\"_denseList\"]:not([class*=\"_portal\"]),[role=\"tree\"][class*=\"_menu\"]{-webkit-backdrop-filter:var(--dsh-any-blur-header,none);backdrop-filter:var(--dsh-any-blur-header,none)}";
2851
+ const EXEMPT_DEFAULT_RULE = "[role=\"dialog\"][aria-modal=\"true\"]:not([aria-labelledby]){backdrop-filter:none;-webkit-backdrop-filter:none}[role=\"menu\"]:has([class*=\"_danger\"]){backdrop-filter:none;-webkit-backdrop-filter:none;-webkit-text-stroke-width:0}";
2852
+ /** Selector the dynamic header token rule targets — kept in sync with the
2853
+ * static HEADER_POPOVER_RULE selector list (specificity equal or higher than
2854
+ * the card group's `[role="menu"]:not(...)` so it wins). */
2855
+ const HEADER_TOKEN_SELECTOR = "[data-dsh-any-header-popover][role],ul[class*=\"_menu\"],div[role=\"dialog\"][class*=\"_panel\"]:not([aria-modal=\"true\"]),[role=\"menu\"][class*=\"_denseList\"]:not([class*=\"_portal\"]),[role=\"tree\"][class*=\"_menu\"]";
2856
+ /** Header-popover surfaces: retint the menu token and (re)write the blur.
2857
+ * Mirrors applyProduced: the static rule above never changes, so a drag only
2858
+ * rewrites the blur variable and this one dynamic rule. */
2859
+ function applyHeaderPopovers() {
2860
+ const root = document.documentElement;
2861
+ const px = rBlurs().header;
2862
+ if (px > 0) root.style.setProperty("--dsh-any-blur-header", `blur(${px}px)`);
2863
+ else root.style.removeProperty("--dsh-any-blur-header");
2864
+ let opacity = rHeaderOpacity();
2865
+ if (opacity < 0) opacity = 0;
2866
+ if (opacity > 1) opacity = 1;
2867
+ const menu = (paletteTokens() ?? readHostOpacityTokens())?.["--dsw-specific-menu"];
2868
+ const el = ensureHeaderStyle();
2869
+ const css = menu === void 0 ? "" : `${HEADER_TOKEN_SELECTOR}{--dsw-specific-menu:${toRgba(menu, opacity)}}`;
2870
+ if (el.textContent !== css) el.textContent = css;
2871
+ }
2872
+ let headerStyleEl = null;
2873
+ function ensureHeaderStyle() {
2874
+ if (headerStyleEl?.isConnected) return headerStyleEl;
2875
+ headerStyleEl = document.createElement("style");
2876
+ headerStyleEl.dataset.plugin = "dsh-any-background-header";
2877
+ document.head.appendChild(headerStyleEl);
2878
+ return headerStyleEl;
2879
+ }
2880
+ /** Pin the exempt groups (confirm dialogs and the session-row menu) so no
2881
+ * appearance slider moves them — while KEEPING the active theme color.
2882
+ *
2883
+ * "Default" here means "not faded, not blurred", NOT "host-palette original":
2884
+ * an earlier version restored the :root host colors, which also stripped the
2885
+ * picked theme color and made the menu look un-themed. The correct source is
2886
+ * the same one every other slider uses (the plugin palette when there is one,
2887
+ * else the host's resolved tokens) with alpha pinned to 1 — so the surface
2888
+ * keeps whatever color the theme currently dictates and only loses the slider's
2889
+ * translucency and blur.
2890
+ *
2891
+ * Emitted as REAL literals into a dedicated stylesheet, never as
2892
+ * `var(--x, var(--x))`: a self-referencing fallback is a CSS cycle that
2893
+ * computes to the guaranteed-invalid value and turns every consuming
2894
+ * `background:var(--dsw-alias-bg-layer-2)` fully transparent. Literals also
2895
+ * mean that when no color can be resolved we emit NO declaration at all, so
2896
+ * the surface keeps the re-scoped value rather than going transparent. */
2897
+ function applyExemptDefaults() {
2898
+ if (typeof document === "undefined") return;
2899
+ const source = paletteTokens() ?? readHostOpacityTokens();
2900
+ const el = ensureExemptStyle();
2901
+ const decls = [];
2902
+ const add = (token, value) => {
2903
+ if (value === void 0 || value === "") return;
2904
+ decls.push(`${token}:${toRgba(value, 1)}`);
2905
+ };
2906
+ add("--dsw-alias-bg-layer-1", source?.["--dsw-alias-bg-layer-1"]);
2907
+ add("--dsw-alias-bg-layer-2", source?.["--dsw-alias-bg-layer-2"]);
2908
+ add("--dsw-alias-bg-layer-3", source?.["--dsw-alias-bg-layer-3"]);
2909
+ add("--dsw-specific-menu", source?.["--dsw-specific-menu"]);
2910
+ if (decls.length === 0) {
2911
+ if (el.textContent !== "") el.textContent = "";
2912
+ return;
2913
+ }
2914
+ const dialog = "[role=\"dialog\"][aria-modal=\"true\"]:not([aria-labelledby])";
2915
+ const body = decls.join(";");
2916
+ const css = `${dialog},${dialog} *{${body}}[role="menu"]:has([class*="_danger"]){${body}}`;
2917
+ if (el.textContent !== css) el.textContent = css;
2918
+ }
2919
+ let exemptStyleEl = null;
2920
+ function ensureExemptStyle() {
2921
+ if (exemptStyleEl?.isConnected) return exemptStyleEl;
2922
+ exemptStyleEl = document.createElement("style");
2923
+ exemptStyleEl.dataset.plugin = "dsh-any-background-exempt";
2924
+ document.head.appendChild(exemptStyleEl);
2925
+ return exemptStyleEl;
2926
+ }
2927
+ const PRODUCED_RULE = "[data-code-block-content] pre,[data-code-block-banner],[data-composer-chip],[data-changed-files]>button:first-child,[data-terminal],[data-read],[data-context-injection-body],[data-search],[data-web],[class*=\"_ioCard\"]{-webkit-backdrop-filter:var(--dsh-any-blur-prod,none);backdrop-filter:var(--dsh-any-blur-prod,none)}@supports (background:color-mix(in srgb,red 50%,transparent)){.md-code-block{background:transparent!important}.md-code-block>div{background-color:transparent!important}.md-code-block [data-code-block-content] pre{background-color:color-mix(in srgb,var(--dsl-code-block-background,var(--dsw-alias-markdown-code-block,transparent)) var(--dsh-any-prod-pct,100%),transparent)!important}.md-code-block [data-code-block-banner]{background-color:color-mix(in srgb,var(--dsl-code-block-banner-background-color,var(--dsw-alias-markdown-code-block-banner,transparent)) var(--dsh-any-prod-pct,100%),transparent)!important}[data-code-block-content]{--shiki-background:color-mix(in srgb,var(--dsw-alias-markdown-code-block,var(--dsl-code-block-background,transparent)) var(--dsh-any-prod-pct,100%),transparent)}:not(pre)>code{background-color:color-mix(in srgb,var(--dsw-alias-markdown-inline-code,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-composer-chip]>*{background-color:color-mix(in srgb,var(--dsw-alias-interactive-bg-hover,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-terminal]{background-color:color-mix(in srgb,var(--dsw-alias-markdown-code-block,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-terminal] button{background-color:transparent!important}[data-read]{background-color:color-mix(in srgb,var(--dsw-alias-markdown-code-block,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-read]>div:first-child{background-color:color-mix(in srgb,var(--dsw-alias-markdown-code-block-banner,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-context-injection-body]{background-color:color-mix(in srgb,var(--dsw-alias-markdown-code-block,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-search]{background-color:color-mix(in srgb,var(--dsw-alias-markdown-code-block,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[data-web]{background-color:color-mix(in srgb,var(--dsw-alias-markdown-code-block,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}[class*=\"_ioCard\"]{background-color:color-mix(in srgb,var(--dsw-alias-markdown-code-block,transparent) var(--dsh-any-prod-pct,100%),transparent)!important}}";
2671
2928
  const STROKE_RULE = [
2672
2929
  `${SETTINGS_PANEL_SEL}{-webkit-text-stroke:var(--dsh-any-stroke-settings-w,0px) var(--dsh-any-stroke-settings-c,transparent);paint-order:stroke fill}`,
2673
2930
  "[role=\"menu\"]:not([data-dockkit-tab-menu]),[role=\"listbox\"],body>[role=\"tree\"],body>[role=\"dialog\"]:not([aria-modal=\"true\"]){-webkit-text-stroke:var(--dsh-any-stroke-card-w,0px) var(--dsh-any-stroke-card-c,transparent);paint-order:stroke fill}",
2674
2931
  "[data-composer-card],[data-cordis-panel],[data-composer-card] textarea,[data-composer-card] input,[data-composer-card] [contenteditable],[data-cordis-panel] input,[data-cordis-panel] textarea{-webkit-text-stroke:var(--dsh-any-stroke-input-w,0px) var(--dsh-any-stroke-input-c,transparent);paint-order:stroke fill}",
2675
2932
  `[data-chat-flow]{-webkit-text-stroke:var(--dsh-any-stroke-chat-w,0px) var(--dsh-any-stroke-chat-c,transparent);paint-order:stroke fill}`,
2676
2933
  `[data-conversation-composer-overlay]{-webkit-text-stroke:var(--dsh-any-stroke-trajectory-w,0px) var(--dsh-any-stroke-trajectory-c,transparent);paint-order:stroke fill}`,
2677
- "[data-code-block-content] pre,[data-code-block-content],[data-code-block-banner],[data-composer-chip],:not(pre)>code{-webkit-text-stroke:var(--dsh-any-stroke-produced-w,0px) var(--dsh-any-stroke-produced-c,transparent);paint-order:stroke fill}",
2934
+ "[data-code-block-content] pre,[data-code-block-content],[data-code-block-banner],[data-composer-chip],:not(pre)>code,[data-changed-files],[data-terminal],[data-read],[data-context-injection-body],[data-search],[data-web],[class*=\"_ioCard\"]{-webkit-text-stroke:var(--dsh-any-stroke-produced-w,0px) var(--dsh-any-stroke-produced-c,transparent);paint-order:stroke fill}",
2678
2935
  `[data-dsh-bottom-panel],[data-sidebar-right-panel]{-webkit-text-stroke:var(--dsh-any-stroke-panel-w,0px) var(--dsh-any-stroke-panel-c,transparent);paint-order:stroke fill}`,
2936
+ "[data-dsh-any-header-popover],ul[class*=\"_menu\"],div[role=\"dialog\"][class*=\"_panel\"]:not([aria-modal=\"true\"]),[role=\"menu\"][class*=\"_denseList\"]:not([class*=\"_portal\"]),[role=\"tree\"][class*=\"_menu\"]{-webkit-text-stroke:var(--dsh-any-stroke-header-w,0px) var(--dsh-any-stroke-header-c,transparent);paint-order:stroke fill}",
2937
+ "[data-chat-flow] [role=\"status\"],[data-chat-flow] [data-turn-process],[data-chat-flow] [data-text-shimmer],[data-chat-flow] [style*=\"--dsh-text-shimmer-spread\"]{-webkit-text-stroke-width:0!important;paint-order:normal!important}",
2679
2938
  `svg{-webkit-text-stroke-width:0!important;paint-order:normal!important}`,
2680
2939
  `::placeholder{-webkit-text-stroke-width:0!important}`
2681
2940
  ].join("");
@@ -2689,7 +2948,8 @@ window.__ModuleLoader__.load({
2689
2948
  "trajectory",
2690
2949
  "input",
2691
2950
  "panel",
2692
- "produced"
2951
+ "produced",
2952
+ "header"
2693
2953
  ];
2694
2954
  /** Resolve one group's stroke color key into a concrete CSS color.
2695
2955
  * 'auto' contrasts the FONT direction (white fonts → black stroke and vice
@@ -2724,6 +2984,18 @@ window.__ModuleLoader__.load({
2724
2984
  }
2725
2985
  /** Write every group's stroke width/color variables + the two column strokes.
2726
2986
  * Called from applyWp so palette/verdict changes re-derive 'auto'/'theme'. */
2987
+ /** Write (or clear) one column's inline text stroke. The stroke rides inline
2988
+ * styles and is fully removed at width 0 so nothing lingers after the slider
2989
+ * resets. */
2990
+ function applyInlineStroke(el, s) {
2991
+ if (s.width > 0) {
2992
+ el.style.setProperty("-webkit-text-stroke", `${s.width}px ${strokeColor(s)}`);
2993
+ el.style.setProperty("paint-order", "stroke fill");
2994
+ } else {
2995
+ el.style.removeProperty("-webkit-text-stroke");
2996
+ el.style.removeProperty("paint-order");
2997
+ }
2998
+ }
2727
2999
  function applyStrokes() {
2728
3000
  const strokes = rStrokes();
2729
3001
  const root = document.documentElement;
@@ -2735,15 +3007,7 @@ window.__ModuleLoader__.load({
2735
3007
  discoverParts();
2736
3008
  for (const g of STROKE_INLINE_GROUPS) {
2737
3009
  const el = g === "bg" ? centerEl : sidebarEl;
2738
- if (el === null) continue;
2739
- const s = strokes[g];
2740
- if (s.width > 0) {
2741
- el.style.setProperty("-webkit-text-stroke", `${s.width}px ${strokeColor(s)}`);
2742
- el.style.setProperty("paint-order", "stroke fill");
2743
- } else {
2744
- el.style.removeProperty("-webkit-text-stroke");
2745
- el.style.removeProperty("paint-order");
2746
- }
3010
+ if (el !== null) applyInlineStroke(el, strokes[g]);
2747
3011
  }
2748
3012
  }
2749
3013
  /** Live per-group stroke update during slider drag (no full re-apply). */
@@ -2756,13 +3020,7 @@ window.__ModuleLoader__.load({
2756
3020
  discoverParts();
2757
3021
  const el = part === "bg" ? centerEl : sidebarEl;
2758
3022
  if (el === null) return;
2759
- if (s.width > 0) {
2760
- el.style.setProperty("-webkit-text-stroke", `${s.width}px ${strokeColor(s)}`);
2761
- el.style.setProperty("paint-order", "stroke fill");
2762
- } else {
2763
- el.style.removeProperty("-webkit-text-stroke");
2764
- el.style.removeProperty("paint-order");
2765
- }
3023
+ applyInlineStroke(el, s);
2766
3024
  }
2767
3025
  /** Teardown only: drop every stroke variable and column inline stroke. */
2768
3026
  function removeStrokes() {
@@ -2867,27 +3125,35 @@ window.__ModuleLoader__.load({
2867
3125
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-blur-panel", `blur(${px}px)`);
2868
3126
  else document.documentElement.style.removeProperty("--dsh-any-blur-panel");
2869
3127
  }
2870
- function applySettingsOverrides(op) {
3128
+ /** Re-scope one surface group's layer tokens onto plugin-owned variables so a
3129
+ * dedicated slider owns its alpha (the host has no per-surface opacity). The
3130
+ * settings group additionally retints its dialog surface variable. Always
3131
+ * written explicitly — including at 100% — so removing a slider's effect means
3132
+ * writing 1, not deleting the variable (the style rules below have no fallback
3133
+ * and would otherwise resolve the body tokens that applyCustomTokens rewrote
3134
+ * with the homepage card alpha). */
3135
+ function applyLayerOverrides(prefix, op, surface) {
2871
3136
  const [h, s, l] = rColor();
2872
3137
  const tokens = genTokens(h, s, l, rColorScheme()).tokens;
2873
- const layer1 = tokens["--dsw-alias-bg-layer-1"];
2874
- const layer2 = tokens["--dsw-alias-bg-layer-2"];
2875
- const layer3 = tokens["--dsw-alias-bg-layer-3"];
2876
- if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-surface", toRgba(layer2, op));
2877
- if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-1", toRgba(layer1, op));
2878
- if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-2", toRgba(layer2, op));
2879
- if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-3", toRgba(layer3, op));
3138
+ if (surface) {
3139
+ const surfaceColor = tokens["--dsw-alias-bg-layer-2"];
3140
+ if (surfaceColor !== void 0) document.documentElement.style.setProperty(`${prefix}-surface`, toRgba(surfaceColor, op));
3141
+ }
3142
+ for (const layer of [
3143
+ 1,
3144
+ 2,
3145
+ 3
3146
+ ]) {
3147
+ const c = tokens[`--dsw-alias-bg-layer-${layer}`];
3148
+ if (c !== void 0) document.documentElement.style.setProperty(`${prefix}-layer-${layer}`, toRgba(c, op));
3149
+ }
3150
+ }
3151
+ function applySettingsOverrides(op) {
3152
+ applyLayerOverrides("--dsh-any-bg-settings", op, true);
2880
3153
  }
2881
3154
  const TRAJECTORY_STYLE_RULE = "[data-conversation-composer-overlay]{--dsw-alias-bg-layer-1:var(--dsh-any-traj-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-traj-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-traj-layer-3)}";
2882
3155
  function applyTrajectoryOverrides(op) {
2883
- const [h, s, l] = rColor();
2884
- const tokens = genTokens(h, s, l, rColorScheme()).tokens;
2885
- const layer1 = tokens["--dsw-alias-bg-layer-1"];
2886
- const layer2 = tokens["--dsw-alias-bg-layer-2"];
2887
- const layer3 = tokens["--dsw-alias-bg-layer-3"];
2888
- if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-1", toRgba(layer1, op));
2889
- if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-2", toRgba(layer2, op));
2890
- if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-3", toRgba(layer3, op));
3156
+ applyLayerOverrides("--dsh-any-traj", op, false);
2891
3157
  }
2892
3158
  /** Register a callback fired when a wallpaper-extracted theme color is
2893
3159
  * adopted by the auto-adaptation path, so the section can re-register the
@@ -2896,6 +3162,9 @@ window.__ModuleLoader__.load({
2896
3162
  let colorAdoptedListener = null;
2897
3163
  function onColorAdopted(cb) {
2898
3164
  colorAdoptedListener = cb;
3165
+ return () => {
3166
+ if (colorAdoptedListener === cb) colorAdoptedListener = null;
3167
+ };
2899
3168
  }
2900
3169
  /** Apply the theme color: use the saved pick directly, or fall back to
2901
3170
  * extracting a dominant color from the current wallpaper. */
@@ -2911,8 +3180,8 @@ window.__ModuleLoader__.load({
2911
3180
  if (hsl && rWp() === url) {
2912
3181
  cfg.color = hsl;
2913
3182
  colorAdoptedListener?.(hsl);
3183
+ applyWp();
2914
3184
  }
2915
- applyWp();
2916
3185
  });
2917
3186
  } else applyWp();
2918
3187
  }
@@ -2944,7 +3213,7 @@ window.__ModuleLoader__.load({
2944
3213
  * preserving the user's scale/intensity/speed/density/preset choices. */
2945
3214
  function regenerateGeneratedBg() {
2946
3215
  const params = cfg.generatedBg;
2947
- if (!params || cfg.backgroundType === "image") return;
3216
+ if (!params || cfg.backgroundType === "image" || cfg.backgroundType === "video") return;
2948
3217
  cfg.generatedBg = {
2949
3218
  ...params,
2950
3219
  seed: randomSeed()
@@ -2967,9 +3236,10 @@ window.__ModuleLoader__.load({
2967
3236
  wpEl.appendChild(wpController.canvas);
2968
3237
  }
2969
3238
  requestAnimationFrame(() => {
2970
- if (!wpController) return;
2971
3239
  const controller = wpController;
3240
+ if (controller === null) return;
2972
3241
  const frame = controller.snapshot();
3242
+ if (wpController !== controller) return;
2973
3243
  setWpUrl(frame);
2974
3244
  applyWp();
2975
3245
  snapshotListener?.();
@@ -2996,35 +3266,36 @@ window.__ModuleLoader__.load({
2996
3266
  partBlurStyleEl.textContent = PART_BLUR_RULE;
2997
3267
  document.head.appendChild(partBlurStyleEl);
2998
3268
  }
2999
- /** Find one direct child of `frame` whose `[data-rightbar-col]` marker matches.
3000
- * The rightbar is its own grid track, NOT a "details" twin of the center
3001
- * column, so it must never receive the main-bg tint or blur underlay. */
3002
- function findRightbarInFrame(frame) {
3003
- for (let i = 0; i < frame.children.length; i++) {
3004
- const child = frame.children[i];
3005
- if (child instanceof HTMLElement && child.dataset.rightbarCol !== void 0) return child;
3006
- }
3007
- return null;
3008
- }
3009
3269
  function discoverParts() {
3010
3270
  const overlay = document.querySelector("[data-shell-overlay]");
3011
3271
  if (overlay === null) return;
3012
3272
  const frame = overlay.parentElement;
3013
3273
  if (frame === null) return;
3014
3274
  frameEl = frame;
3015
- rightEl = findRightbarInFrame(frame);
3016
- const idx = Array.from(frame.children).indexOf(overlay);
3017
- if (rightEl !== null) {
3018
- const rightIdx = Array.from(frame.children).indexOf(rightEl);
3019
- sidebarEl = frame.children[rightIdx - 2] ?? null;
3020
- centerEl = frame.children[rightIdx - 1] ?? null;
3275
+ let overlayIdx = -1;
3276
+ let rightIdx = -1;
3277
+ let right = null;
3278
+ const children = frame.children;
3279
+ for (let i = 0; i < children.length; i++) {
3280
+ const child = children[i];
3281
+ if (child === overlay) overlayIdx = i;
3282
+ else if (child instanceof HTMLElement && child.dataset.rightbarCol !== void 0) {
3283
+ right = child;
3284
+ rightIdx = i;
3285
+ }
3286
+ }
3287
+ rightEl = right;
3288
+ if (right !== null) {
3289
+ sidebarEl = children[rightIdx - 2] ?? null;
3290
+ centerEl = children[rightIdx - 1] ?? null;
3021
3291
  } else {
3022
- sidebarEl = frame.children[idx - 3] ?? null;
3023
- centerEl = frame.children[idx - 2] ?? null;
3292
+ sidebarEl = children[overlayIdx - 3] ?? null;
3293
+ centerEl = children[overlayIdx - 2] ?? null;
3024
3294
  }
3025
3295
  }
3026
3296
  function setBlur(el, px) {
3027
3297
  if (el === null) return;
3298
+ if (px === 0 && !el.classList.contains(PART_BLUR_CLASS) && el.getAttribute("data-dab-pos-patched") !== "1") return;
3028
3299
  const underlay = el.querySelector(`:scope > .${PART_UNDERLAY_CLASS}`);
3029
3300
  if (px > 0) {
3030
3301
  ensurePartBlurStyle();
@@ -3092,6 +3363,7 @@ window.__ModuleLoader__.load({
3092
3363
  applyInputBlur(blurs.input);
3093
3364
  applyPanelBlur(blurs.panel);
3094
3365
  applyProduced();
3366
+ applyHeaderPopovers();
3095
3367
  applyViewCards();
3096
3368
  }
3097
3369
  /** Produced/artifact surfaces (conversation code blocks + their banner +
@@ -3132,6 +3404,10 @@ window.__ModuleLoader__.load({
3132
3404
  applyProduced();
3133
3405
  return;
3134
3406
  }
3407
+ if (part === "header") {
3408
+ applyHeaderPopovers();
3409
+ return;
3410
+ }
3135
3411
  if (part === "chat" || part === "trajectory") {
3136
3412
  applyViewCards();
3137
3413
  return;
@@ -3158,6 +3434,34 @@ window.__ModuleLoader__.load({
3158
3434
  plain: true
3159
3435
  }];
3160
3436
  const viewTargets = VIEW_CARDS.map(() => null);
3437
+ /** View specs whose real host marker has been seen at least once. Until a
3438
+ * marker is seen, its absence is ambiguous — "that view is not mounted" or
3439
+ * "this build has no such marker, keep running the generic fallback" — and the
3440
+ * pass has to keep probing. Once seen, absence can only mean the former, so the
3441
+ * pass may be skipped.
3442
+ *
3443
+ * This matters because the two specs are two different MAIN VIEWS and only one
3444
+ * of them is mounted at a time (the host renders the main slot for the active
3445
+ * panel id, keyed by `entryKey` — see AppFrame's MainPanel). So "every surface
3446
+ * present" is never true during a normal session, and an all-or-nothing gate on
3447
+ * it silently never engages: that is exactly how the previous version of this
3448
+ * observer ended up running the full pass on every animation frame while
3449
+ * tokens streamed. */
3450
+ const markerSeen = /* @__PURE__ */ new Set();
3451
+ /** Resolve just the stable host markers for the view specs — one querySelector
3452
+ * each, no fallback heuristics. Cheap enough to run on every coalesced burst,
3453
+ * and enough to notice a view mounting/unmounting. */
3454
+ function probeViewMarkers() {
3455
+ const center = centerEl;
3456
+ if (center === null || !document.body.contains(center)) return VIEW_CARDS.map(() => null);
3457
+ return VIEW_CARDS.map((spec) => center.querySelector(spec.sel));
3458
+ }
3459
+ /** Whether the pass must run regardless of the value/identity signature. */
3460
+ function needsPartsPass(markers) {
3461
+ if (!columnsPresent()) return true;
3462
+ if (markers.some((el, i) => el !== null && el !== viewTargets[i])) return true;
3463
+ return VIEW_CARDS.some((spec, i) => spec.fallback === true && markers[i] === null && !markerSeen.has(i));
3464
+ }
3161
3465
  function isScrollableY(el) {
3162
3466
  const oy = getComputedStyle(el).overflowY;
3163
3467
  return oy === "auto" || oy === "scroll" || oy === "overlay";
@@ -3202,6 +3506,7 @@ window.__ModuleLoader__.load({
3202
3506
  const marked = centerEl.querySelector(spec.sel);
3203
3507
  const cached = viewTargets[idx];
3204
3508
  if (marked !== null) {
3509
+ markerSeen.add(idx);
3205
3510
  if (cached !== null && cached !== marked) {
3206
3511
  setBlur(cached, 0);
3207
3512
  restoreCardHost(cached, spec.mark, spec.prev, spec.plain === true);
@@ -3337,15 +3642,48 @@ window.__ModuleLoader__.load({
3337
3642
  syncTableFix();
3338
3643
  }
3339
3644
  let partsObserver = null;
3645
+ let partsApplyRaf = 0;
3646
+ /** Signature of the last blur+opacity pass we actually wrote to the DOM. */
3647
+ let appliedPartsKey = "";
3648
+ /** The surfaces the last pass styled, compared BY IDENTITY: the host can swap
3649
+ * a view element for a fresh one while the slider values stay identical, and
3650
+ * the value signature alone would then skip the pass and leave the new view
3651
+ * unstyled until a slider moves. */
3652
+ let appliedTargets = [];
3653
+ /** The four frame columns are mounted and still attached. These are the frame's
3654
+ * own children (see the host's AppFrame: sidebar / center / rightbar carrying
3655
+ * `data-rightbar-col` / the overlay), so they exist as soon as the shell does —
3656
+ * unlike the per-view cards below, which come and go with the active view. */
3657
+ function columnsPresent() {
3658
+ return frameEl !== null && sidebarEl !== null && centerEl !== null && rightEl !== null && document.body.contains(frameEl) && document.body.contains(centerEl);
3659
+ }
3660
+ /** Snapshot of every surface the pass would style, for the identity check. */
3661
+ function targetsNow() {
3662
+ return [
3663
+ frameEl,
3664
+ sidebarEl,
3665
+ centerEl,
3666
+ rightEl,
3667
+ ...viewTargets
3668
+ ];
3669
+ }
3340
3670
  /** Watch for the AppFrame mounting so persisted blurs land even when the shell
3341
- * renders after this plugin's apply. Cheap: once all parts are found, the
3342
- * callback returns. */
3671
+ * renders after this plugin's apply. */
3343
3672
  function watchParts() {
3344
3673
  if (partsObserver !== null || typeof MutationObserver === "undefined") return;
3345
3674
  partsObserver = new MutationObserver(() => {
3346
- if (frameEl !== null && sidebarEl !== null && centerEl !== null && rightEl !== null && document.body.contains(frameEl) && viewTargets.every((el) => el !== null && document.body.contains(el))) return;
3347
- applyPartBlurs(rBlurs());
3348
- applyPartOpacities(rOps());
3675
+ if (partsApplyRaf !== 0) return;
3676
+ partsApplyRaf = requestAnimationFrame(() => {
3677
+ partsApplyRaf = 0;
3678
+ const blurs = rBlurs();
3679
+ const ops = rOps();
3680
+ const key = JSON.stringify([blurs, ops]);
3681
+ if (!needsPartsPass(probeViewMarkers()) && key === appliedPartsKey && !targetsNow().some((el, i) => el !== appliedTargets[i])) return;
3682
+ applyPartBlurs(blurs);
3683
+ applyPartOpacities(ops);
3684
+ appliedPartsKey = key;
3685
+ appliedTargets = targetsNow();
3686
+ });
3349
3687
  });
3350
3688
  partsObserver.observe(document.body, {
3351
3689
  childList: true,
@@ -3355,6 +3693,13 @@ window.__ModuleLoader__.load({
3355
3693
  function stopWatchingParts() {
3356
3694
  partsObserver?.disconnect();
3357
3695
  partsObserver = null;
3696
+ if (partsApplyRaf !== 0) {
3697
+ cancelAnimationFrame(partsApplyRaf);
3698
+ partsApplyRaf = 0;
3699
+ }
3700
+ appliedPartsKey = "";
3701
+ appliedTargets = [];
3702
+ markerSeen.clear();
3358
3703
  }
3359
3704
  let themeObserver = null;
3360
3705
  let themeRaf = 0;
@@ -3507,6 +3852,9 @@ window.__ModuleLoader__.load({
3507
3852
  * be re-registered through the host theme service. */
3508
3853
  function onVerdictApplied(cb) {
3509
3854
  verdictListener = cb;
3855
+ return () => {
3856
+ if (verdictListener === cb) verdictListener = null;
3857
+ };
3510
3858
  }
3511
3859
  function applyVerdict(dark) {
3512
3860
  if (rBgDark() === dark) return;
@@ -3634,6 +3982,8 @@ window.__ModuleLoader__.load({
3634
3982
  clearVideoEl();
3635
3983
  wpEl?.remove();
3636
3984
  wpEl = null;
3985
+ wpVerdict = null;
3986
+ setBgDark(null);
3637
3987
  }
3638
3988
  } else if (cfg.backgroundType !== "image" && cfg.generatedBg) {
3639
3989
  clearVideoEl();
@@ -3659,6 +4009,9 @@ window.__ModuleLoader__.load({
3659
4009
  applyTrajectoryOverrides(rTrajectoryOpacity());
3660
4010
  }
3661
4011
  applyPanelOverrides(rPanelOpacity());
4012
+ applyProduced();
4013
+ applyHeaderPopovers();
4014
+ applyExemptDefaults();
3662
4015
  applyPartBlurs(rBlurs());
3663
4016
  applyStrokes();
3664
4017
  }
@@ -3695,6 +4048,11 @@ window.__ModuleLoader__.load({
3695
4048
  document.documentElement.style.removeProperty("--dsh-any-blur-panel");
3696
4049
  document.documentElement.style.removeProperty("--dsh-any-blur-prod");
3697
4050
  document.documentElement.style.removeProperty("--dsh-any-prod-pct");
4051
+ document.documentElement.style.removeProperty("--dsh-any-blur-header");
4052
+ headerStyleEl?.remove();
4053
+ headerStyleEl = null;
4054
+ exemptStyleEl?.remove();
4055
+ exemptStyleEl = null;
3698
4056
  for (const v of Object.values(OPACITY_VARS)) document.documentElement.style.removeProperty(v);
3699
4057
  baseTokenKey = "";
3700
4058
  lastBgKey = "";
@@ -3705,8 +4063,14 @@ window.__ModuleLoader__.load({
3705
4063
  pendingOps = null;
3706
4064
  tableFixStyleEl?.remove();
3707
4065
  tableFixStyleEl = null;
4066
+ partBlurStyleEl?.remove();
4067
+ partBlurStyleEl = null;
3708
4068
  removeStrokes();
3709
4069
  removeFontFace();
4070
+ imgNat = null;
4071
+ lowResUrl = null;
4072
+ lowResFor = "";
4073
+ dragLow = false;
3710
4074
  setBlur(frameEl, 0);
3711
4075
  setBlur(sidebarEl, 0);
3712
4076
  setBlur(centerEl, 0);
@@ -3842,7 +4206,10 @@ window.__ModuleLoader__.load({
3842
4206
  /* ── wheel card ──────────────────────────────────────────────────────────── */
3843
4207
  .dab-wheel-card{position:relative;display:flex;align-items:center;justify-content:center;gap:28px;flex-wrap:wrap;padding:24px 18px}
3844
4208
  .dab-wheel-glow{position:absolute;width:230px;height:230px;border-radius:50%;filter:blur(48px);opacity:.2;background:var(--c,#888);pointer-events:none;transition:background .4s ease}
3845
- .dab-wheel{position:relative;cursor:crosshair;border-radius:50%;box-shadow:0 12px 32px -14px rgba(0,0,0,.4)}
4209
+ /* flex:none is load bearing, not cosmetic: the pointer maths reads client
4210
+ * coordinates straight off this element's box, so any shrink below the 220px
4211
+ * drawing surface would desynchronise the ring from the cursor. */
4212
+ .dab-wheel{position:relative;flex:none;cursor:crosshair;border-radius:50%;box-shadow:0 12px 32px -14px rgba(0,0,0,.4)}
3846
4213
  .dab-hint{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);padding:0 4px}
3847
4214
 
3848
4215
  /* ── precise color inputs ────────────────────────────────────────────────── */
@@ -3896,7 +4263,7 @@ window.__ModuleLoader__.load({
3896
4263
  .dab-slider::-moz-range-thumb{width:13px;height:13px;border-radius:50%;background:var(--dsw-alias-button-elevated-fill,#fff);border:2px solid var(--dsw-alias-brand-primary)}
3897
4264
 
3898
4265
  /* ── interface part cards ────────────────────────────────────────────────── */
3899
- .dab-grid-parts{display:grid;grid-template-columns:repeat(auto-fill,minmax(256px,1fr));gap:13px}
4266
+ .dab-grid-parts{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(240px,100%),1fr));gap:13px}
3900
4267
  .dab-part-head{display:flex;align-items:center;gap:11px;margin-bottom:14px}
3901
4268
  .dab-part-ico{width:32px;height:32px;flex:none;border-radius:10px;display:grid;place-items:center;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)}
3902
4269
  .dab-part-name{font-size:13.5px;font-weight:600}
@@ -3973,7 +4340,7 @@ window.__ModuleLoader__.load({
3973
4340
  .dab-spin{animation:dab-rotate .7s cubic-bezier(.3,.7,.3,1) 1}
3974
4341
 
3975
4342
  /* ── profile page ────────────────────────────────────────────────────────── */
3976
- .dab-profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(238px,1fr));gap:13px}
4343
+ .dab-profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:13px}
3977
4344
  .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)}
3978
4345
  .dab-profile-title{font-size:14px;font-weight:650}
3979
4346
  .dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
@@ -3982,7 +4349,7 @@ window.__ModuleLoader__.load({
3982
4349
 
3983
4350
  /* ── presets / profiles / schedule / rotation ────────────────────────────── */
3984
4351
  .dab-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
3985
- .dab-preset-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
4352
+ .dab-preset-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(150px,100%),1fr));gap:10px}
3986
4353
  .dab-preset{display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding:12px;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:13px;cursor:pointer;font:inherit;text-align:left;transition:border-color .25s,transform .25s,box-shadow .25s;animation:dab-row-in .34s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--i,0) * 45ms)}
3987
4354
  .dab-preset:hover{transform:translateY(-2px);border-color:var(--dsw-alias-brand-primary);box-shadow:0 8px 22px -12px rgba(0,0,0,.3)}
3988
4355
  .dab-preset-dot{width:22px;height:22px;border-radius:50%;margin-bottom:4px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.12),0 4px 10px -4px rgba(0,0,0,.35);background:var(--dsw-alias-bg-layer-2);display:grid;place-items:center;color:var(--dsw-alias-label-tertiary)}
@@ -4009,7 +4376,7 @@ window.__ModuleLoader__.load({
4009
4376
  .dab-schedule-wrap.is-open{grid-template-rows:1fr;visibility:visible;transition:grid-template-rows .42s cubic-bezier(.22,1,.36,1)}
4010
4377
  .dab-schedule-clip{overflow:hidden;min-height:0;opacity:0;transition:opacity .28s ease}
4011
4378
  .dab-schedule-wrap.is-open .dab-schedule-clip{opacity:1;transition:opacity .32s ease .1s}
4012
- .dab-schedule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;padding-top:12px}
4379
+ .dab-schedule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:16px;padding-top:12px}
4013
4380
  .dab-schedule-cell{min-width:0;animation:dab-row-in .34s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--i,0) * 60ms)}
4014
4381
  .dab-time-row{display:flex;flex-direction:column;gap:9px}
4015
4382
  .dab-time-label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
@@ -4045,6 +4412,9 @@ window.__ModuleLoader__.load({
4045
4412
  .dab-crash{display:flex;flex-direction:column;gap:10px;align-items:center;padding:28px 18px;border:1px solid var(--dsw-alias-border-l2);border-radius:16px}
4046
4413
  .dab-crash-title{font-size:15px;font-weight:650}
4047
4414
  .dab-crash-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.5}
4415
+ /* The crash cause: readable, scrollable, and clipped to a few lines so a long
4416
+ * stack cannot push the reset button off the panel. */
4417
+ .dab-crash-detail{margin:0;max-width:min(520px,80vw);max-height:132px;overflow:auto;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);font-family:var(--dab-mono);font-size:10.5px;line-height:1.5;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));text-align:left;white-space:pre-wrap;word-break:break-word}
4048
4418
 
4049
4419
  /* ── keyframes ───────────────────────────────────────────────────────────── */
4050
4420
  @keyframes dab-fade-in{from{opacity:0}to{opacity:1}}
@@ -4060,6 +4430,72 @@ window.__ModuleLoader__.load({
4060
4430
  @keyframes dab-zoom-in{from{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:none}}
4061
4431
  @keyframes dab-flow{to{background-position:300% 50%}}
4062
4432
 
4433
+ /* ── sidebar surface (a dsh-better-sidebar page) ─────────────────────────────
4434
+ * Same shell, hosted in a panel that is handed a full-height column (the host's
4435
+ * native tab body is height:100%) and whose width the user drags freely. So only
4436
+ * THREE things are fixed here, all of them structural:
4437
+ * 1. the shell fills the height and the page body owns the scroll
4438
+ * (flex:1 + min-height:0), so the panel stays one scrollable column instead
4439
+ * of pushing the whole tab into a scrollbar;
4440
+ * 2. the nav is a rail ABOVE the page rather than beside it — a side rail would
4441
+ * spend 150px of a panel that is narrow by nature, and this way the page
4442
+ * keeps the full width for its own grids;
4443
+ * 3. long tokens (paths, hex, URLs) may wrap instead of widening the panel.
4444
+ *
4445
+ * Everything else — grid columns, paddings, type scale, whether the cards lift on
4446
+ * hover — is width-driven through the container queries below, which measure this
4447
+ * shell (the .dab-root container, which declares container-type:inline-size) and
4448
+ * not the viewport. A narrow sidebar compacts; a wide one falls back to the
4449
+ * dialog's own spacing and multi-column grids, because at that point it *is* a
4450
+ * dialog-sized surface. */
4451
+ [data-dab-surface="sidebar"].dab-root{height:100%;min-height:0;padding:12px 12px 0}
4452
+ [data-dab-surface="sidebar"] .dab-shell{display:flex;flex-direction:column;align-items:stretch;gap:11px;height:100%;min-height:0;max-width:none;padding-bottom:0}
4453
+ [data-dab-surface="sidebar"] .dab-nav{gap:0}
4454
+ [data-dab-surface="sidebar"] .dab-nav-list{flex-direction:row;gap:2px;overflow-x:auto;overflow-y:hidden;padding-bottom:1px}
4455
+ [data-dab-surface="sidebar"] .dab-nav-list::-webkit-scrollbar{display:none}
4456
+ [data-dab-surface="sidebar"] .dab-nav-ind{display:none}
4457
+ [data-dab-surface="sidebar"] .dab-nav-item{flex:none;height:32px;gap:7px;padding:0 10px;font-size:12px}
4458
+ [data-dab-surface="sidebar"] .dab-nav-item.is-active{background:var(--dsw-alias-bg-layer-2)}
4459
+ [data-dab-surface="sidebar"] .dab-page{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding-bottom:16px;scrollbar-width:thin}
4460
+ /* Last-resort safety for text that cannot wrap (paths, hex, URLs). */
4461
+ [data-dab-surface="sidebar"] .dab-hint,[data-dab-surface="sidebar"] .dab-part-name{overflow-wrap:anywhere}
4462
+ /* The panel's width is whatever the user dragged it to, so let the part cards
4463
+ * fill as many columns as actually fit instead of hard-coding one. 170px is the
4464
+ * narrowest a part card stays readable once its header can wrap (see the head
4465
+ * rules below), which puts the second column at roughly a 380px panel — the
4466
+ * default right-sidebar width. */
4467
+ [data-dab-surface="sidebar"] .dab-grid-parts{grid-template-columns:repeat(auto-fill,minmax(min(170px,100%),1fr));gap:12px}
4468
+ [data-dab-surface="sidebar"] .dab-types{grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr))}
4469
+ [data-dab-surface="sidebar"] .dab-card{padding:12px}
4470
+ [data-dab-surface="sidebar"] .dab-part-head{gap:8px;margin-bottom:10px}
4471
+ [data-dab-surface="sidebar"] .dab-part-ico{width:26px;height:26px;border-radius:8px}
4472
+ [data-dab-surface="sidebar"] .dab-part-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px}
4473
+ [data-dab-surface="sidebar"] .dab-part-badge{padding:2px 7px;font-size:10px}
4474
+
4475
+ /* Narrow panel: tighten the chrome so the content still has room. */
4476
+ @container (max-width:539px){
4477
+ [data-dab-surface="sidebar"] .dab-page{gap:11px}
4478
+ [data-dab-surface="sidebar"] .dab-h1{font-size:17px}
4479
+ [data-dab-surface="sidebar"] .dab-desc{font-size:12px}
4480
+ [data-dab-surface="sidebar"] .dab-orb-wrap{width:96px;height:96px}
4481
+ [data-dab-surface="sidebar"] .dab-wheel-card{padding:14px 10px;gap:16px}
4482
+ [data-dab-surface="sidebar"] .dab-wheel-glow{width:170px;height:170px}
4483
+ /* Lift-on-hover is a dialog affordance; in a cramped scrolling panel it only
4484
+ * adds jitter under the cursor. */
4485
+ [data-dab-surface="sidebar"] .dab-card-hover:hover{transform:none;box-shadow:none}
4486
+ }
4487
+ /* Roomy panel: behave like the dialog again — its spacing, its type scale, and
4488
+ * multi-column grids (a smaller track minimum than the dialog, because the page
4489
+ * here is a few hundred px narrower than a dialog's content column). */
4490
+ @container (min-width:540px){
4491
+ [data-dab-surface="sidebar"] .dab-shell{gap:16px}
4492
+ [data-dab-surface="sidebar"] .dab-page{gap:13px}
4493
+ [data-dab-surface="sidebar"] .dab-nav-item{height:34px;font-size:12.5px;padding:0 12px}
4494
+ /* The shared ≤620px rule flattens the type cards to one column, which is right
4495
+ * for a narrow phone-width dialog but wasteful in a roomy panel. */
4496
+ [data-dab-surface="sidebar"] .dab-types{grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}
4497
+ }
4498
+
4063
4499
  /* ── responsive & motion preferences ─────────────────────────────────────── */
4064
4500
  @container (max-width:620px){
4065
4501
  .dab-shell{grid-template-columns:1fr;gap:14px}
@@ -4074,6 +4510,30 @@ window.__ModuleLoader__.load({
4074
4510
  @container (min-width:621px){
4075
4511
  .dab-shell{grid-template-columns:158px minmax(0,1fr);gap:26px}
4076
4512
  }
4513
+ /* Phone: on a handset the settings dialog's content column shrinks to roughly
4514
+ * the viewport, and the dialog-sized chrome (18px cards, 21px headings, a 118px
4515
+ * orb, 24px paddings) stops fitting — cards end up wider than the column or the
4516
+ * color row wraps into three cramped pieces. These are the same tightenings the
4517
+ * sidebar surface applies at its own narrow widths, so a phone settings dialog
4518
+ * and a narrow sidebar panel end up looking the same. */
4519
+ @container (max-width:480px){
4520
+ .dab-shell{gap:10px}
4521
+ .dab-page{gap:10px}
4522
+ .dab-head{margin:0 0 2px}
4523
+ .dab-h1{font-size:18px}
4524
+ .dab-desc{font-size:12px}
4525
+ .dab-card{padding:14px;border-radius:14px}
4526
+ .dab-hero-accent{gap:16px}
4527
+ .dab-orb-wrap{width:92px;height:92px}
4528
+ .dab-wheel-card{padding:14px 10px;gap:16px}
4529
+ .dab-wheel-glow{width:180px;height:180px}
4530
+ /* Three fixed type-card columns need ~400px; two adaptive ones fit 320px. */
4531
+ .dab-types{grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr))}
4532
+ .dab-grid-parts{gap:11px}
4533
+ .dab-part-head{margin-bottom:10px}
4534
+ .dab-slider-block + .dab-slider-block{margin-top:10px}
4535
+ .dab-footer{padding-top:10px}
4536
+ }
4077
4537
  @media (prefers-reduced-motion:reduce){
4078
4538
  .dab-root *,.dab-root *::before,.dab-root *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}
4079
4539
  }
@@ -4404,25 +4864,32 @@ window.__ModuleLoader__.load({
4404
4864
  this.props.onReset?.();
4405
4865
  };
4406
4866
  render() {
4407
- if (this.state.error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4408
- className: "dab-crash",
4409
- children: [
4410
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4411
- className: "dab-crash-title",
4412
- children: this.props.t("crashTitle")
4413
- }),
4414
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4415
- className: "dab-crash-desc",
4416
- children: this.props.t("crashDesc")
4417
- }),
4418
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4419
- type: "button",
4420
- className: "dab-btn",
4421
- onClick: this.reset,
4422
- children: this.props.t("crashReset")
4423
- })
4424
- ]
4425
- });
4867
+ if (this.state.error) {
4868
+ const detail = this.state.error.stack ?? `${this.state.error.name}: ${this.state.error.message}`;
4869
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4870
+ className: "dab-crash",
4871
+ children: [
4872
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4873
+ className: "dab-crash-title",
4874
+ children: this.props.t("crashTitle")
4875
+ }),
4876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4877
+ className: "dab-crash-desc",
4878
+ children: this.props.t("crashDesc")
4879
+ }),
4880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
4881
+ className: "dab-crash-detail",
4882
+ children: detail
4883
+ }),
4884
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4885
+ type: "button",
4886
+ className: "dab-btn",
4887
+ onClick: this.reset,
4888
+ children: this.props.t("crashReset")
4889
+ })
4890
+ ]
4891
+ });
4892
+ }
4426
4893
  return this.props.children;
4427
4894
  }
4428
4895
  };
@@ -4571,6 +5038,10 @@ window.__ModuleLoader__.load({
4571
5038
  }, [col]);
4572
5039
  const pendingRef = (0, react.useRef)(null);
4573
5040
  const rafRef = (0, react.useRef)(null);
5041
+ /** Unmount-time cleanup for a drag that never saw its mouseup (panel closed
5042
+ * mid-drag): without it the document listeners stay attached forever and the
5043
+ * pending rAF keeps calling setState on a dead component. */
5044
+ const dragCleanupRef = (0, react.useRef)(null);
4574
5045
  const flushPending = (0, react.useCallback)(() => {
4575
5046
  rafRef.current = null;
4576
5047
  const p = pendingRef.current;
@@ -4612,13 +5083,25 @@ window.__ModuleLoader__.load({
4612
5083
  schedule(colRef.current.hue, s, l);
4613
5084
  }
4614
5085
  };
4615
- const onUp = () => {
5086
+ const stopDrag = () => {
4616
5087
  document.removeEventListener("mousemove", onMove);
4617
- document.removeEventListener("mouseup", onUp);
5088
+ document.removeEventListener("mouseup", stopDrag);
5089
+ dragCleanupRef.current = null;
4618
5090
  };
5091
+ dragCleanupRef.current = stopDrag;
4619
5092
  document.addEventListener("mousemove", onMove);
4620
- document.addEventListener("mouseup", onUp);
5093
+ document.addEventListener("mouseup", stopDrag);
4621
5094
  }, [schedule]);
5095
+ (0, react.useEffect)(() => {
5096
+ return () => {
5097
+ if (rafRef.current !== null) {
5098
+ cancelAnimationFrame(rafRef.current);
5099
+ rafRef.current = null;
5100
+ }
5101
+ pendingRef.current = null;
5102
+ dragCleanupRef.current?.();
5103
+ };
5104
+ }, []);
4622
5105
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
4623
5106
  ref: cvsRef,
4624
5107
  width: WHEEL_SIZE,
@@ -4667,6 +5150,7 @@ window.__ModuleLoader__.load({
4667
5150
  },
4668
5151
  onChange: (e) => {
4669
5152
  setText(e.target.value);
5153
+ if (e.target.value === "") return;
4670
5154
  const v = Number(e.target.value);
4671
5155
  if (Number.isFinite(v)) onChange(clamp(v, min, max));
4672
5156
  }
@@ -4783,9 +5267,6 @@ window.__ModuleLoader__.load({
4783
5267
  //#region src/client/components/ColorPicker.tsx
4784
5268
  const MAG_SIZE = 96;
4785
5269
  const MAG_ZOOM = 8;
4786
- function toHex$1(rgb) {
4787
- return "#" + rgb.map((v) => v.toString(16).padStart(2, "0")).join("");
4788
- }
4789
5270
  /**
4790
5271
  * Eyedropper modal: shows the wallpaper full-bleed (no drag/zoom) and lets the
4791
5272
  * user click any pixel to adopt it as the theme color. A magnifier circle
@@ -4815,7 +5296,7 @@ window.__ModuleLoader__.load({
4815
5296
  canvas.height = imgRef.current.naturalHeight;
4816
5297
  const ctx = canvas.getContext("2d");
4817
5298
  if (ctx) ctx.drawImage(imgRef.current, 0, 0);
4818
- }, [ready]);
5299
+ }, [ready, url]);
4819
5300
  const sampleAt = (0, react.useCallback)((clientX, clientY) => {
4820
5301
  const canvas = canvasRef.current;
4821
5302
  if (!canvas) return null;
@@ -4930,14 +5411,14 @@ window.__ModuleLoader__.load({
4930
5411
  height: 28,
4931
5412
  borderRadius: 8,
4932
5413
  border: "1px solid rgba(255,255,255,0.4)",
4933
- background: hover ? toHex$1(hover.rgb) : "transparent"
5414
+ background: hover ? rgbToHex(hover.rgb) : "transparent"
4934
5415
  } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4935
5416
  style: {
4936
5417
  color: "#fff",
4937
5418
  fontSize: 13,
4938
5419
  fontFamily: "var(--dab-mono, monospace)"
4939
5420
  },
4940
- children: hover ? `${toHex$1(hover.rgb)} · rgb(${hover.rgb.join(", ")})` : "—"
5421
+ children: hover ? `${rgbToHex(hover.rgb)} · rgb(${hover.rgb.join(", ")})` : "—"
4941
5422
  })]
4942
5423
  }),
4943
5424
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -5020,9 +5501,6 @@ window.__ModuleLoader__.load({
5020
5501
  .58
5021
5502
  ]
5022
5503
  ];
5023
- function toHex(rgb) {
5024
- return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
5025
- }
5026
5504
  function ColorPage({ p, notify }) {
5027
5505
  const { t, hue, sat, lit, setColor, extractColor, setSchemeOverride, useStore } = p;
5028
5506
  const storeUrl = useStore((s) => s.url);
@@ -5037,7 +5515,7 @@ window.__ModuleLoader__.load({
5037
5515
  ];
5038
5516
  const [h, s, l] = hsvToHsl(wheel[0], wheel[1], wheel[2]);
5039
5517
  const [r, g, b] = hslToRgb(h, s, l);
5040
- const hex = toHex([
5518
+ const hex = rgbToHex([
5041
5519
  r,
5042
5520
  g,
5043
5521
  b
@@ -5167,7 +5645,7 @@ window.__ModuleLoader__.load({
5167
5645
  type: "button",
5168
5646
  className: `dab-swatch${on ? " is-on" : ""}`,
5169
5647
  style: { background: `hsl(${sh} ${Math.round(ss * 100)}% ${Math.round(sl * 100)}%)` },
5170
- title: toHex(hslToRgb(sh, ss, sl)).toUpperCase(),
5648
+ title: rgbToHex(hslToRgb(sh, ss, sl)).toUpperCase(),
5171
5649
  onClick: () => setColor(...hslToHsv(sh, ss, sl))
5172
5650
  }, i);
5173
5651
  })
@@ -5242,15 +5720,16 @@ window.__ModuleLoader__.load({
5242
5720
  const paint = (el, v) => {
5243
5721
  el.style.setProperty("--pct", String((v - min) / (max - min) * 100));
5244
5722
  };
5723
+ const fmtRef = (0, react.useRef)(fmt);
5724
+ fmtRef.current = fmt;
5245
5725
  (0, react.useEffect)(() => {
5246
5726
  if (inputRef.current) {
5247
5727
  inputRef.current.value = String(def);
5248
5728
  paint(inputRef.current, def);
5249
5729
  }
5250
- if (valRef.current) valRef.current.textContent = fmt(def);
5730
+ if (valRef.current) valRef.current.textContent = fmtRef.current(def);
5251
5731
  }, [
5252
5732
  def,
5253
- fmt,
5254
5733
  min,
5255
5734
  max
5256
5735
  ]);
@@ -5319,12 +5798,16 @@ window.__ModuleLoader__.load({
5319
5798
  * signal and the body childList observer fires exactly when it appears. The
5320
5799
  * panel surfaces we actually style are listed only as reinforcement: they are
5321
5800
  * conditionally rendered (collapsed panels drop them), so they must never be
5322
- * the sole signal. */
5801
+ * the sole signal.
5802
+ *
5803
+ * `[data-sidebar-right-panel]` is deliberately ABSENT: on every host generation
5804
+ * it marks the host's own right Sidebar (always present once a session opens),
5805
+ * so counting it would make the better-sidebar verdict permanently true and the
5806
+ * panel slider could never drop back to its native "右方侧边栏" identity. */
5323
5807
  const BETTER_SIDEBAR_MARKERS = [
5324
5808
  "[data-dsh-better-sidebar]",
5325
5809
  "[data-dsh-panel-host]",
5326
- "[data-dsh-bottom-panel]",
5327
- "[data-sidebar-right-panel]"
5810
+ "[data-dsh-bottom-panel]"
5328
5811
  ];
5329
5812
  let betterSidebar = false;
5330
5813
  const subs = /* @__PURE__ */ new Set();
@@ -5360,6 +5843,15 @@ window.__ModuleLoader__.load({
5360
5843
  observer.observe(document.body, { childList: true });
5361
5844
  return () => observer.disconnect();
5362
5845
  }
5846
+ let nativeTabs = false;
5847
+ const nativeSubs = /* @__PURE__ */ new Set();
5848
+ /** Mark the native right-Sidebar service as confirmed. Sticky — the service
5849
+ * never goes away within a session. */
5850
+ function markNativeSidebarTabs() {
5851
+ if (nativeTabs) return;
5852
+ nativeTabs = true;
5853
+ nativeSubs.forEach((cb) => cb());
5854
+ }
5363
5855
  //#endregion
5364
5856
  //#region src/client/components/pages/InterfacePage.tsx
5365
5857
  const PARTS$1 = [
@@ -5403,6 +5895,11 @@ window.__ModuleLoader__.load({
5403
5895
  labelKey: "uiProduced",
5404
5896
  Icon: TextIcon
5405
5897
  },
5898
+ {
5899
+ isHeader: true,
5900
+ labelKey: "uiHeader",
5901
+ Icon: PanelIcon
5902
+ },
5406
5903
  {
5407
5904
  isPanel: true,
5408
5905
  labelKey: "uiPanelRegion",
@@ -5410,9 +5907,9 @@ window.__ModuleLoader__.load({
5410
5907
  }
5411
5908
  ];
5412
5909
  function InterfacePage({ p }) {
5413
- const { t, setOps, setBlurs, setSop, setPanelOp } = p;
5910
+ const { t, setOps, setBlurs, setSop, setPanelOp, useStore } = p;
5911
+ useStore((s) => s.metaRev);
5414
5912
  const hasBetterSidebar = useBetterSidebar();
5415
- const parts = PARTS$1.filter((part) => !(part.isPanel && !hasBetterSidebar));
5416
5913
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
5417
5914
  className: "dab-head dab-rise",
5418
5915
  style: { "--d": 0 },
@@ -5432,11 +5929,12 @@ window.__ModuleLoader__.load({
5432
5929
  ]
5433
5930
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5434
5931
  className: "dab-grid-parts",
5435
- children: parts.map((part, i) => {
5436
- const { labelKey, Icon, isSettings, isChat, isTrajectory, isPanel, isProduced } = part;
5932
+ children: PARTS$1.map((part, i) => {
5933
+ const { labelKey, Icon, isSettings, isChat, isTrajectory, isPanel, isProduced, isHeader } = part;
5437
5934
  const opKey = part.opKey;
5438
- const blurKey = isChat ? "chat" : isTrajectory ? "trajectory" : isSettings ? "settings" : isPanel ? "panel" : isProduced ? "produced" : opKey;
5439
- const opacity = isChat ? rChatTextOpacity() : isTrajectory ? rTrajectoryOpacity() : isSettings ? rSop() : isPanel ? rPanelOpacity() : isProduced ? rProducedOpacity() : rOps()[opKey];
5935
+ const label = isPanel ? t(hasBetterSidebar ? "uiPanelRegion" : "uiPanelNative") : t(labelKey);
5936
+ const blurKey = isChat ? "chat" : isTrajectory ? "trajectory" : isSettings ? "settings" : isPanel ? "panel" : isProduced ? "produced" : isHeader ? "header" : opKey;
5937
+ const opacity = isChat ? rChatTextOpacity() : isTrajectory ? rTrajectoryOpacity() : isSettings ? rSop() : isPanel ? rPanelOpacity() : isProduced ? rProducedOpacity() : isHeader ? rHeaderOpacity() : rOps()[opKey];
5440
5938
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5441
5939
  className: "dab-card dab-card-hover dab-rise",
5442
5940
  style: { "--d": i + 1 },
@@ -5450,7 +5948,7 @@ window.__ModuleLoader__.load({
5450
5948
  }),
5451
5949
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5452
5950
  className: "dab-part-name",
5453
- children: t(labelKey)
5951
+ children: label
5454
5952
  }),
5455
5953
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5456
5954
  className: "dab-part-badge",
@@ -5482,6 +5980,9 @@ window.__ModuleLoader__.load({
5482
5980
  } else if (isProduced) {
5483
5981
  cfg.producedOpacity = op;
5484
5982
  applyProduced();
5983
+ } else if (isHeader) {
5984
+ cfg.headerOpacity = op;
5985
+ applyHeaderPopovers();
5485
5986
  } else {
5486
5987
  const ops = { ...rOps() };
5487
5988
  ops[opKey] = op;
@@ -5506,6 +6007,10 @@ window.__ModuleLoader__.load({
5506
6007
  cfg.producedOpacity = op;
5507
6008
  applyProduced();
5508
6009
  saveConfig();
6010
+ } else if (isHeader) {
6011
+ cfg.headerOpacity = op;
6012
+ applyHeaderPopovers();
6013
+ saveConfig();
5509
6014
  } else {
5510
6015
  const ops = { ...rOps() };
5511
6016
  ops[opKey] = op;
@@ -5581,6 +6086,11 @@ window.__ModuleLoader__.load({
5581
6086
  labelKey: "uiProduced",
5582
6087
  Icon: TextIcon
5583
6088
  },
6089
+ {
6090
+ key: "header",
6091
+ labelKey: "uiHeader",
6092
+ Icon: PanelIcon
6093
+ },
5584
6094
  {
5585
6095
  key: "panel",
5586
6096
  labelKey: "uiPanelRegion",
@@ -5617,7 +6127,7 @@ window.__ModuleLoader__.load({
5617
6127
  }
5618
6128
  }
5619
6129
  function FontPage({ p, notify }) {
5620
- const { t, setFont, removeFont, setFontEnabled, setStrokes } = p;
6130
+ const { t, setFont, removeFont, setFontEnabled, setStrokes, useStore } = p;
5621
6131
  const hasBetterSidebar = useBetterSidebar();
5622
6132
  const fileRef = (0, react.useRef)(null);
5623
6133
  const [strokes, setStrokesState] = (0, react.useState)(() => rStrokes());
@@ -5627,6 +6137,15 @@ window.__ModuleLoader__.load({
5627
6137
  name: null
5628
6138
  }));
5629
6139
  const [busy, setBusy] = (0, react.useState)(false);
6140
+ const metaRev = useStore((s) => s.metaRev);
6141
+ (0, react.useEffect)(() => {
6142
+ setStrokesState(rStrokes());
6143
+ setFontInfo((prev) => prev.mime === cfg.fontMime && prev.enabled === cfg.fontEnabled ? prev : {
6144
+ mime: cfg.fontMime,
6145
+ enabled: cfg.fontEnabled,
6146
+ name: null
6147
+ });
6148
+ }, [metaRev]);
5630
6149
  const hasFont = fontInfo.mime !== null;
5631
6150
  const fontLabel = fontInfo.name ?? (fontInfo.mime !== null ? fontInfo.mime.replace("font/", "").toUpperCase() : t("fontNone"));
5632
6151
  /** Write one group's stroke: cfg + live DOM now, disk on commit. `live`
@@ -5651,9 +6170,9 @@ window.__ModuleLoader__.load({
5651
6170
  ...fontInfo,
5652
6171
  name: file.name
5653
6172
  });
5654
- const ok = await setFont(file);
6173
+ const outcome = await setFont(file);
5655
6174
  setBusy(false);
5656
- if (ok) setFontInfo({
6175
+ if (outcome.ok) setFontInfo({
5657
6176
  mime: cfg.fontMime,
5658
6177
  enabled: cfg.fontEnabled,
5659
6178
  name: file.name
@@ -5664,7 +6183,7 @@ window.__ModuleLoader__.load({
5664
6183
  enabled: cfg.fontEnabled,
5665
6184
  name: null
5666
6185
  });
5667
- notify(t("fontFail"), false);
6186
+ notify(uploadRefusalText(outcome, t, "fontFail"), false);
5668
6187
  }
5669
6188
  };
5670
6189
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
@@ -5878,6 +6397,19 @@ window.__ModuleLoader__.load({
5878
6397
  });
5879
6398
  const containerRef = (0, react.useRef)(null);
5880
6399
  const imgRef = (0, react.useRef)(null);
6400
+ /**
6401
+ * The overlay node is held in STATE rather than a plain ref, so the listener
6402
+ * effect below re-runs the moment it actually exists.
6403
+ *
6404
+ * This is not a style choice. Everything here renders through <Portal>, which
6405
+ * returns null on its first render — its own effect has to create the host
6406
+ * node under <html> first. A plain ref is therefore still null when a
6407
+ * mount-only effect runs, and an effect whose deps never change again attaches
6408
+ * nothing, ever. That is exactly how the touch handlers below ended up dead on
6409
+ * real phones: the wheel handler only survived because its deps churn on every
6410
+ * zoom/pan, so the wallpaper's img.onload re-ran it once and wired it up.
6411
+ */
6412
+ const [overlayEl, setOverlayEl] = (0, react.useState)(null);
5881
6413
  const dragRef = (0, react.useRef)({
5882
6414
  active: false,
5883
6415
  sx: 0,
@@ -5885,6 +6417,16 @@ window.__ModuleLoader__.load({
5885
6417
  spx: 0,
5886
6418
  spy: 0
5887
6419
  });
6420
+ /** Unmount-time cleanup for a drag that never saw its mouseup (editor closed
6421
+ * mid-drag): without it the document listeners stay attached forever and
6422
+ * keep calling setPos on a dead component. */
6423
+ const dragStopRef = (0, react.useRef)(null);
6424
+ /** Set once a gesture actually moved, so the click that follows a drag is not
6425
+ * mistaken for a tap on the backdrop (mousedown and mouseup in different
6426
+ * subtrees make the click land on the overlay, i.e. "outside"). Reset at the
6427
+ * start of every gesture — touchstart / mousedown / wheel — so it can only
6428
+ * ever describe the gesture that just ended, never an earlier one. */
6429
+ const movedRef = (0, react.useRef)(false);
5888
6430
  const viewRef = (0, react.useRef)({
5889
6431
  zoom,
5890
6432
  pos
@@ -5930,6 +6472,7 @@ window.__ModuleLoader__.load({
5930
6472
  ]);
5931
6473
  const onDown = (0, react.useCallback)((e) => {
5932
6474
  e.preventDefault();
6475
+ movedRef.current = false;
5933
6476
  dragRef.current = {
5934
6477
  active: true,
5935
6478
  sx: e.clientX,
@@ -5939,58 +6482,50 @@ window.__ModuleLoader__.load({
5939
6482
  };
5940
6483
  const onMove = (ev) => {
5941
6484
  if (!dragRef.current.active) return;
6485
+ if (Math.abs(ev.clientX - dragRef.current.sx) > 4 || Math.abs(ev.clientY - dragRef.current.sy) > 4) movedRef.current = true;
5942
6486
  setPos({
5943
6487
  x: dragRef.current.spx + ev.clientX - dragRef.current.sx,
5944
6488
  y: dragRef.current.spy + ev.clientY - dragRef.current.sy
5945
6489
  });
5946
6490
  };
5947
- const onUp = () => {
6491
+ const stopDrag = () => {
5948
6492
  dragRef.current.active = false;
5949
6493
  document.removeEventListener("mousemove", onMove);
5950
- document.removeEventListener("mouseup", onUp);
6494
+ document.removeEventListener("mouseup", stopDrag);
6495
+ dragStopRef.current = null;
5951
6496
  };
6497
+ dragStopRef.current = stopDrag;
5952
6498
  document.addEventListener("mousemove", onMove);
5953
- document.addEventListener("mouseup", onUp);
6499
+ document.addEventListener("mouseup", stopDrag);
5954
6500
  }, [pos]);
6501
+ (0, react.useEffect)(() => () => {
6502
+ dragStopRef.current?.();
6503
+ }, []);
5955
6504
  const onWheelCb = (0, react.useCallback)((e) => {
5956
6505
  e.preventDefault();
6506
+ movedRef.current = false;
5957
6507
  const el = containerRef.current;
5958
6508
  if (!el) return;
5959
6509
  const rect = el.getBoundingClientRect();
5960
6510
  const mx = rect.width / 2, my = rect.height / 2;
5961
6511
  const factor = e.deltaY > 0 ? .97 : 1.03;
5962
- const nz = clampZoom(zoom * factor);
5963
- const nx = mx - (mx - pos.x) * (nz / zoom);
5964
- const ny = my - (my - pos.y) * (nz / zoom);
6512
+ const { zoom: z, pos: p } = viewRef.current;
6513
+ const nz = clampZoom(z * factor);
6514
+ const nx = mx - (mx - p.x) * (nz / z);
6515
+ const ny = my - (my - p.y) * (nz / z);
5965
6516
  setZoom(nz);
5966
6517
  setPos({
5967
6518
  x: nx,
5968
6519
  y: ny
5969
6520
  });
5970
- }, [zoom, pos]);
5971
- (0, react.useEffect)(() => {
5972
- const el = containerRef.current;
5973
- if (!el) return;
5974
- el.addEventListener("wheel", onWheelCb, { passive: false });
5975
- return () => el.removeEventListener("wheel", onWheelCb);
5976
- }, [onWheelCb]);
6521
+ }, []);
5977
6522
  const onTouchStartCb = (0, react.useCallback)((e) => {
5978
- const el = containerRef.current;
5979
- if (!el) return;
5980
- if (e.touches.length === 1) {
5981
- pinchRef.current = null;
5982
- const tp = e.touches[0];
5983
- touchDragRef.current = {
5984
- sx: tp.clientX,
5985
- sy: tp.clientY,
5986
- spx: viewRef.current.pos.x,
5987
- spy: viewRef.current.pos.y
5988
- };
5989
- return;
5990
- }
6523
+ const card = containerRef.current;
6524
+ if (!card) return;
6525
+ movedRef.current = false;
5991
6526
  if (e.touches.length >= 2) {
5992
6527
  touchDragRef.current = null;
5993
- const rect = el.getBoundingClientRect();
6528
+ const rect = card.getBoundingClientRect();
5994
6529
  const a = e.touches[0], b = e.touches[1];
5995
6530
  pinchRef.current = {
5996
6531
  dist: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY),
@@ -6000,16 +6535,32 @@ window.__ModuleLoader__.load({
6000
6535
  px: viewRef.current.pos.x,
6001
6536
  py: viewRef.current.pos.y
6002
6537
  };
6538
+ return;
6539
+ }
6540
+ if (e.touches.length === 1) {
6541
+ pinchRef.current = null;
6542
+ if (!card.contains(e.target)) {
6543
+ touchDragRef.current = null;
6544
+ return;
6545
+ }
6546
+ const tp = e.touches[0];
6547
+ touchDragRef.current = {
6548
+ sx: tp.clientX,
6549
+ sy: tp.clientY,
6550
+ spx: viewRef.current.pos.x,
6551
+ spy: viewRef.current.pos.y
6552
+ };
6003
6553
  }
6004
6554
  }, []);
6005
6555
  const onTouchMoveCb = (0, react.useCallback)((e) => {
6006
- const el = containerRef.current;
6007
- if (!el) return;
6556
+ const card = containerRef.current;
6557
+ if (!card) return;
6008
6558
  if (e.touches.length >= 2 && pinchRef.current !== null) {
6009
6559
  e.preventDefault();
6560
+ movedRef.current = true;
6010
6561
  const p = pinchRef.current;
6011
6562
  if (p.dist <= 0) return;
6012
- const rect = el.getBoundingClientRect();
6563
+ const rect = card.getBoundingClientRect();
6013
6564
  const a = e.touches[0], b = e.touches[1];
6014
6565
  const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
6015
6566
  const cx = (a.clientX + b.clientX) / 2 - rect.left;
@@ -6026,6 +6577,7 @@ window.__ModuleLoader__.load({
6026
6577
  e.preventDefault();
6027
6578
  const d = touchDragRef.current;
6028
6579
  const tp = e.touches[0];
6580
+ if (Math.abs(tp.clientX - d.sx) > 4 || Math.abs(tp.clientY - d.sy) > 4) movedRef.current = true;
6029
6581
  setPos({
6030
6582
  x: d.spx + tp.clientX - d.sx,
6031
6583
  y: d.spy + tp.clientY - d.sy
@@ -6048,19 +6600,29 @@ window.__ModuleLoader__.load({
6048
6600
  }
6049
6601
  }, []);
6050
6602
  (0, react.useEffect)(() => {
6051
- const el = containerRef.current;
6052
- if (!el) return;
6053
- el.addEventListener("touchstart", onTouchStartCb, { passive: false });
6054
- el.addEventListener("touchmove", onTouchMoveCb, { passive: false });
6055
- el.addEventListener("touchend", onTouchEndCb, { passive: false });
6056
- el.addEventListener("touchcancel", onTouchEndCb, { passive: false });
6603
+ if (!overlayEl) return;
6604
+ const noPageGesture = (e) => e.preventDefault();
6605
+ overlayEl.addEventListener("wheel", onWheelCb, { passive: false });
6606
+ overlayEl.addEventListener("touchstart", onTouchStartCb, { passive: false });
6607
+ overlayEl.addEventListener("touchmove", onTouchMoveCb, { passive: false });
6608
+ overlayEl.addEventListener("touchend", onTouchEndCb, { passive: false });
6609
+ overlayEl.addEventListener("touchcancel", onTouchEndCb, { passive: false });
6610
+ overlayEl.addEventListener("gesturestart", noPageGesture, { passive: false });
6611
+ overlayEl.addEventListener("gesturechange", noPageGesture, { passive: false });
6612
+ overlayEl.addEventListener("gestureend", noPageGesture, { passive: false });
6057
6613
  return () => {
6058
- el.removeEventListener("touchstart", onTouchStartCb);
6059
- el.removeEventListener("touchmove", onTouchMoveCb);
6060
- el.removeEventListener("touchend", onTouchEndCb);
6061
- el.removeEventListener("touchcancel", onTouchEndCb);
6614
+ overlayEl.removeEventListener("wheel", onWheelCb);
6615
+ overlayEl.removeEventListener("touchstart", onTouchStartCb);
6616
+ overlayEl.removeEventListener("touchmove", onTouchMoveCb);
6617
+ overlayEl.removeEventListener("touchend", onTouchEndCb);
6618
+ overlayEl.removeEventListener("touchcancel", onTouchEndCb);
6619
+ overlayEl.removeEventListener("gesturestart", noPageGesture);
6620
+ overlayEl.removeEventListener("gesturechange", noPageGesture);
6621
+ overlayEl.removeEventListener("gestureend", noPageGesture);
6062
6622
  };
6063
6623
  }, [
6624
+ overlayEl,
6625
+ onWheelCb,
6064
6626
  onTouchStartCb,
6065
6627
  onTouchMoveCb,
6066
6628
  onTouchEndCb
@@ -6078,10 +6640,22 @@ window.__ModuleLoader__.load({
6078
6640
  imgSize
6079
6641
  ]);
6080
6642
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Portal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6643
+ ref: setOverlayEl,
6081
6644
  className: "dab-overlay",
6645
+ style: {
6646
+ touchAction: "none",
6647
+ overscrollBehavior: "contain"
6648
+ },
6082
6649
  onClick: (e) => {
6650
+ if (movedRef.current) {
6651
+ movedRef.current = false;
6652
+ return;
6653
+ }
6083
6654
  if (e.target === e.currentTarget) onClose();
6084
6655
  },
6656
+ onMouseDown: () => {
6657
+ movedRef.current = false;
6658
+ },
6085
6659
  children: [
6086
6660
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6087
6661
  className: "dab-overlay-title",
@@ -6177,7 +6751,7 @@ window.__ModuleLoader__.load({
6177
6751
  labelKey: "bgModeCenter"
6178
6752
  }
6179
6753
  ];
6180
- function BackgroundPage({ p }) {
6754
+ function BackgroundPage({ p, notify }) {
6181
6755
  const { t, setWpFromServer, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, setRotation, addRotationItems, removeRotationItem, rotateNow, setVideoFromUrl, useStore } = p;
6182
6756
  const storeUrl = useStore((s) => s.url);
6183
6757
  const backgroundType = useStore((s) => s.backgroundType);
@@ -6194,7 +6768,11 @@ window.__ModuleLoader__.load({
6194
6768
  const [urlVal, setUrlVal] = (0, react.useState)("");
6195
6769
  const [urlBusy, setUrlBusy] = (0, react.useState)(false);
6196
6770
  const [urlErr, setUrlErr] = (0, react.useState)(null);
6771
+ const bgRev = useStore((s) => s.bgRev);
6197
6772
  const [mode, setModeState] = (0, react.useState)(rBgMode());
6773
+ (0, react.useEffect)(() => {
6774
+ setModeState(rBgMode());
6775
+ }, [bgRev]);
6198
6776
  const isVideo = backgroundType === "video";
6199
6777
  const isStatic = backgroundType === "image" || isVideo;
6200
6778
  const isGenerated = !isStatic;
@@ -6208,14 +6786,15 @@ window.__ModuleLoader__.load({
6208
6786
  genPreset,
6209
6787
  generatedBg?.seed
6210
6788
  ]);
6211
- const onFileSelect = (f) => {
6789
+ const onFileSelect = async (f) => {
6212
6790
  if (f.type.startsWith("video/")) {
6213
- setVideo(f, f.type);
6791
+ const outcome = await setVideo(f, f.type);
6792
+ if (!outcome.ok) notify(uploadRefusalText(outcome, t, "videoUploadFail"), false);
6214
6793
  return;
6215
6794
  }
6216
- uploadWallpaper(f).then((ok) => {
6217
- if (ok) setWpFromServer(WALLPAPER_SERVE_URL);
6218
- });
6795
+ const outcome = await uploadWallpaper(f);
6796
+ if (outcome.ok) setWpFromServer(WALLPAPER_SERVE_URL);
6797
+ else notify(uploadRefusalText(outcome, t, "bgUploadFail"), false);
6219
6798
  };
6220
6799
  const onDrop = (e) => {
6221
6800
  e.preventDefault();
@@ -6884,7 +7463,8 @@ window.__ModuleLoader__.load({
6884
7463
  trajectory: 0,
6885
7464
  input: 0,
6886
7465
  panel: 0,
6887
- produced: 0
7466
+ produced: 0,
7467
+ header: 0
6888
7468
  };
6889
7469
  /** Presets ship with strokes off — text outlines are an opt-in look. Fresh
6890
7470
  * objects per call so a profile restore can never alias another preset's
@@ -6904,7 +7484,8 @@ window.__ModuleLoader__.load({
6904
7484
  trajectory: off(),
6905
7485
  input: off(),
6906
7486
  panel: off(),
6907
- produced: off()
7487
+ produced: off(),
7488
+ header: off()
6908
7489
  };
6909
7490
  };
6910
7491
  const BUILTIN_PRESETS = [
@@ -6926,7 +7507,8 @@ window.__ModuleLoader__.load({
6926
7507
  chatTextOpacity: 0,
6927
7508
  trajectoryOpacity: 1,
6928
7509
  panelOpacity: 1,
6929
- producedOpacity: 1
7510
+ producedOpacity: 1,
7511
+ headerOpacity: 1
6930
7512
  }
6931
7513
  },
6932
7514
  {
@@ -6960,7 +7542,8 @@ window.__ModuleLoader__.load({
6960
7542
  chatTextOpacity: 0,
6961
7543
  trajectoryOpacity: .85,
6962
7544
  panelOpacity: .85,
6963
- producedOpacity: 1
7545
+ producedOpacity: 1,
7546
+ headerOpacity: 1
6964
7547
  }
6965
7548
  },
6966
7549
  {
@@ -6981,7 +7564,8 @@ window.__ModuleLoader__.load({
6981
7564
  chatTextOpacity: 0,
6982
7565
  trajectoryOpacity: 1,
6983
7566
  panelOpacity: 1,
6984
- producedOpacity: 1
7567
+ producedOpacity: 1,
7568
+ headerOpacity: 1
6985
7569
  }
6986
7570
  },
6987
7571
  {
@@ -7015,7 +7599,8 @@ window.__ModuleLoader__.load({
7015
7599
  chatTextOpacity: 0,
7016
7600
  trajectoryOpacity: .8,
7017
7601
  panelOpacity: .8,
7018
- producedOpacity: 1
7602
+ producedOpacity: 1,
7603
+ headerOpacity: 1
7019
7604
  }
7020
7605
  },
7021
7606
  {
@@ -7049,7 +7634,8 @@ window.__ModuleLoader__.load({
7049
7634
  chatTextOpacity: 0,
7050
7635
  trajectoryOpacity: .78,
7051
7636
  panelOpacity: .75,
7052
- producedOpacity: 1
7637
+ producedOpacity: 1,
7638
+ headerOpacity: 1
7053
7639
  }
7054
7640
  },
7055
7641
  {
@@ -7074,7 +7660,8 @@ window.__ModuleLoader__.load({
7074
7660
  chatTextOpacity: 0,
7075
7661
  trajectoryOpacity: 1,
7076
7662
  panelOpacity: 1,
7077
- producedOpacity: 1
7663
+ producedOpacity: 1,
7664
+ headerOpacity: 1
7078
7665
  }
7079
7666
  }
7080
7667
  ];
@@ -7489,31 +8076,37 @@ window.__ModuleLoader__.load({
7489
8076
  //#endregion
7490
8077
  //#region src/client/components/ThemeSection.tsx
7491
8078
  /**
7492
- * dsh-any-background — settings section shell.
8079
+ * dsh-any-background — appearance shell for the five pages.
7493
8080
  *
7494
- * The host renders this section only while the settings dialog is open and the
7495
- * section is the active nav entry, so mounting IS being visible. The section is
7496
- * rendered INLINE inside the host settings dialog's content column (the host
7497
- * provides the modal chrome); the shell is a left nav rail + page body. Only
7498
- * the transient toast escapes through a Portal (the shell's container-type
7499
- * containment would otherwise capture its fixed positioning).
8081
+ * Two surfaces render this shell, and only the chrome differs:
8082
+ * · `settings` — injected into the host settings dialog's content column,
8083
+ * which supplies the modal frame (nav rail on the left, brand block, page
8084
+ * max-width, the dialog's own scrolling);
8085
+ * · `sidebar` — a page inside dsh-better-sidebar, where the panel is much
8086
+ * narrower, is handed a full-height column, and has to own its own scroll
8087
+ * region. The brand block is dropped (the sidebar's tab bar already names
8088
+ * the page) and the rail turns into a compact row.
8089
+ * The five pages themselves never branch on the surface: width-driven
8090
+ * adaptation lives in the stylesheet's container queries, which measure the
8091
+ * shell itself rather than assuming either surface's width.
7500
8092
  */
7501
8093
  function ThemeSection(props) {
7502
8094
  ensureUiCss();
7503
8095
  const { t } = props;
8096
+ const surface = props.surface ?? "settings";
7504
8097
  const [page, setPage] = (0, react.useState)(0);
7505
8098
  const [toast, setToast] = (0, react.useState)(null);
7506
8099
  const toastTimer = (0, react.useRef)(void 0);
7507
8100
  (0, react.useEffect)(() => () => window.clearTimeout(toastTimer.current), []);
7508
- const notify = (msg, ok = true) => {
8101
+ const notify = (0, react.useCallback)((msg, ok = true) => {
7509
8102
  setToast({
7510
8103
  msg,
7511
8104
  ok
7512
8105
  });
7513
8106
  window.clearTimeout(toastTimer.current);
7514
8107
  toastTimer.current = window.setTimeout(() => setToast(null), 2600);
7515
- };
7516
- const pages = [
8108
+ }, []);
8109
+ const pages = (0, react.useMemo)(() => [
7517
8110
  {
7518
8111
  label: t("pageColor"),
7519
8112
  Icon: DropletIcon,
@@ -7538,7 +8131,10 @@ window.__ModuleLoader__.load({
7538
8131
  {
7539
8132
  label: t("pageBackground"),
7540
8133
  Icon: PhotoIcon,
7541
- node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BackgroundPage, { p: props })
8134
+ node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BackgroundPage, {
8135
+ p: props,
8136
+ notify
8137
+ })
7542
8138
  },
7543
8139
  {
7544
8140
  label: t("pageProfile"),
@@ -7548,16 +8144,21 @@ window.__ModuleLoader__.load({
7548
8144
  notify
7549
8145
  })
7550
8146
  }
7551
- ];
8147
+ ], [
8148
+ t,
8149
+ props,
8150
+ notify
8151
+ ]);
7552
8152
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ErrorBoundary, {
7553
8153
  t,
7554
8154
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7555
8155
  className: "dab-root",
8156
+ "data-dab-surface": surface,
7556
8157
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7557
8158
  className: "dab-shell",
7558
8159
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
7559
8160
  className: "dab-nav",
7560
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8161
+ children: [surface === "sidebar" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7561
8162
  className: "dab-brand",
7562
8163
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7563
8164
  className: "dab-brand-tile",
@@ -7597,6 +8198,377 @@ window.__ModuleLoader__.load({
7597
8198
  });
7598
8199
  }
7599
8200
  //#endregion
8201
+ //#region src/client/sidebar/store-hook.ts
8202
+ /**
8203
+ * dsh-any-background — selector hook over a host store instance.
8204
+ *
8205
+ * The settings slot kit hands the pages a `useStore(selector)` hook that is
8206
+ * bound to this plugin's store instance by the host renderer. A better-sidebar
8207
+ * page gets no such kit — its tab props carry `ctx`/`store`/`scope` for
8208
+ * better-sidebar's own store — so the same contract is rebuilt here from the
8209
+ * store instance itself, which already IS an observable source
8210
+ * (`getSnapshot`/`subscribe`, see `defineStore` in the host's client store).
8211
+ *
8212
+ * Semantics match the host binding on purpose: reduce the published snapshot
8213
+ * through the selector and compare with `Object.is`, so a page subscribing to
8214
+ * one field does not re-render on unrelated writes.
8215
+ */
8216
+ /**
8217
+ * Bind a store instance to a `useStore(selector)` hook. Call once per store and
8218
+ * share the result: the returned hook keeps one subscription identity for its
8219
+ * whole lifetime.
8220
+ * @param store - observable store instance (`defineStore` handle).
8221
+ * @returns a selector hook reading that instance.
8222
+ */
8223
+ function createStoreHook(store) {
8224
+ const subscribe = (onChange) => store.subscribe(onChange);
8225
+ const { getSnapshot } = store;
8226
+ return function useStoreSelector(selector) {
8227
+ const cache = (0, react.useRef)(null);
8228
+ const read = (0, react.useCallback)(() => {
8229
+ const snapshot = getSnapshot();
8230
+ const hit = cache.current;
8231
+ if (hit !== null && hit.snapshot === snapshot) return hit.value;
8232
+ const value = selector(snapshot);
8233
+ cache.current = {
8234
+ snapshot,
8235
+ value
8236
+ };
8237
+ return value;
8238
+ }, [selector]);
8239
+ return (0, react.useSyncExternalStore)(subscribe, read, read);
8240
+ };
8241
+ }
8242
+ //#endregion
8243
+ //#region src/client/sidebar/tab.tsx
8244
+ /**
8245
+ * dsh-any-background — dsh-better-sidebar integration.
8246
+ *
8247
+ * Publishes the plugin's appearance controls as a sidebar page ("主题") in
8248
+ * better-sidebar's tab registry. The page renders the SAME five-page tree as
8249
+ * the settings section — `ThemeSection` takes a `surface` variant — so there is
8250
+ * one UI to maintain, not two forks of it.
8251
+ *
8252
+ * The dependency is optional at every level, which is why nothing here imports
8253
+ * the package:
8254
+ * · the service is read off the client context at runtime, so a DSH install
8255
+ * without better-sidebar silently registers nothing, and a better-sidebar
8256
+ * that mounts *after* this plugin still gets the page (`ctx.inject` fires
8257
+ * whenever the service shows up, never when it never does);
8258
+ * · the plugin's own types are restated in the minimal local contract below
8259
+ * instead of imported — a value import would be rejected by the host's
8260
+ * client-bundle purity gate, and even a type-only import would make the
8261
+ * package a build-time dependency for everyone who does not run it.
8262
+ */
8263
+ /**
8264
+ * The active locale id, used as a re-render signal: page labels are resolved by
8265
+ * calling `t(...)` at render time, so the props object (and with it the memoized
8266
+ * page tree) has to be rebuilt when the language flips — otherwise the page
8267
+ * would keep painting the previous language until some unrelated store write
8268
+ * happened to re-render it.
8269
+ * @param locale - host locale service.
8270
+ * @returns the active locale id (a stable string, safe for `Object.is`).
8271
+ */
8272
+ function useActiveLocale(locale) {
8273
+ const subscribe = (0, react.useCallback)((onChange) => locale.subscribe(onChange), [locale]);
8274
+ const read = (0, react.useCallback)(() => locale.getSnapshot().active, [locale]);
8275
+ return (0, react.useSyncExternalStore)(subscribe, read, read);
8276
+ }
8277
+ /** Rendered inside a sidebar tab: the five pages, `surface: 'sidebar'` shell.
8278
+ * Shared by the better-sidebar page and the native right-Sidebar tab — both
8279
+ * hand a full-height column that owns its own scroll. */
8280
+ function ThemeTab({ face, useStore, locale }) {
8281
+ const active = useActiveLocale(locale);
8282
+ const props = (0, react.useMemo)(() => ({
8283
+ ...face,
8284
+ useStore
8285
+ }), [
8286
+ face,
8287
+ useStore,
8288
+ active
8289
+ ]);
8290
+ return (0, react.createElement)(ThemeSection, {
8291
+ ...props,
8292
+ surface: "sidebar"
8293
+ });
8294
+ }
8295
+ /**
8296
+ * Register the appearance page with better-sidebar, if it is installed.
8297
+ *
8298
+ * Call from the client `apply`. The registration is owned by the injected
8299
+ * scope's effect, so unloading the plugin (or HMR) withdraws it — without that,
8300
+ * the next activation would throw on the duplicate id.
8301
+ * @param ctx - client context.
8302
+ * @param opts.face - lazy accessor for the shared business face.
8303
+ * @param opts.store - the plugin's store instance, or null on a host where the
8304
+ * store module never resolved (in that case there is nothing to bind and no
8305
+ * page is registered).
8306
+ */
8307
+ function registerThemeSidebarTab(ctx, opts) {
8308
+ const { store } = opts;
8309
+ if (store === null || typeof store.getSnapshot !== "function" || typeof store.subscribe !== "function") {
8310
+ console.warn("dsh-any-background: no observable store instance on this host; the sidebar page stays unregistered");
8311
+ return;
8312
+ }
8313
+ const locale = ctx.locale;
8314
+ const t = locale.bind(NS);
8315
+ const useStore = createStoreHook(store);
8316
+ const ORDER = 55;
8317
+ ctx.inject?.(["betterSidebar"], (scope) => {
8318
+ const service = scope.betterSidebar;
8319
+ if (service === void 0) return;
8320
+ const suppress = suppressesBetterSidebar();
8321
+ if (suppress === true) return;
8322
+ let disposeTab = null;
8323
+ const unsubscribeHost = suppress === null ? subscribeHostInfo(() => {
8324
+ if (suppressesBetterSidebar() === true) {
8325
+ disposeTab?.();
8326
+ disposeTab = null;
8327
+ unsubscribeHost();
8328
+ }
8329
+ }) : () => {};
8330
+ const face = opts.face();
8331
+ scope.effect(() => {
8332
+ disposeTab = service.registerTab({
8333
+ id: "dsh-any-background:theme",
8334
+ title: () => t("nav"),
8335
+ icon: (size) => (0, react.createElement)(SunIcon, { size }),
8336
+ order: ORDER,
8337
+ single: true,
8338
+ component: () => (0, react.createElement)(ThemeTab, {
8339
+ face,
8340
+ useStore,
8341
+ locale
8342
+ })
8343
+ }) ?? null;
8344
+ return () => {
8345
+ unsubscribeHost();
8346
+ disposeTab?.();
8347
+ disposeTab = null;
8348
+ };
8349
+ });
8350
+ });
8351
+ }
8352
+ //#endregion
8353
+ //#region src/client/sidebar/native-tab.tsx
8354
+ /**
8355
+ * dsh-any-background — native right-Sidebar integration (DSH 0.1.6+).
8356
+ *
8357
+ * DSH 0.1.6 ships an official right Sidebar (`dsh-client-ui-sidebar-right`)
8358
+ * with a public two-stage extension path: a static tab type into
8359
+ * `ctx.sidebarRightTabs` — whose `guide` entry draws a card on the Sidebar's
8360
+ * guide page — and a body under the keyed `sidebar.right.pane.tab` seat. This
8361
+ * module registers the plugin's appearance pages through that path, so the
8362
+ * official Sidebar gains a "主题" card that opens the very same five pages the
8363
+ * settings panel shows (one implementation, one state store; editing in either
8364
+ * place shows up in the other).
8365
+ *
8366
+ * The dependency is optional at every level, mirroring `sidebar/tab.tsx`:
8367
+ * · the registry is waited on with `ctx.inject`, so a host without the
8368
+ * right Sidebar (or an older DSH) silently registers nothing, and a
8369
+ * Sidebar that initializes after this plugin still gets the card;
8370
+ * · the service shape is restated as a minimal local contract instead of
8371
+ * imported — a value import would be rejected by the host's client-bundle
8372
+ * purity gate, and even a type-only import would make the package a
8373
+ * build-time dependency for everyone who does not run it.
8374
+ *
8375
+ * When dsh-better-sidebar is installed it brings its own "主题" page (registered
8376
+ * by `sidebar/tab.tsx`), which would duplicate this card on the same guide
8377
+ * page. The native card therefore yields: it is not registered while
8378
+ * better-sidebar is detected, and it withdraws itself if better-sidebar mounts
8379
+ * later (the presence verdict is sticky-true, so no re-registration path is
8380
+ * needed). The Interface page's panel slider follows the same split:
8381
+ * "右方侧边栏" natively, "bettersidebar" when that plugin is present.
8382
+ */
8383
+ /** The tab implementation's identity, and the key the body registers under. */
8384
+ const NATIVE_TAB_ID = "dsh-any-background:theme";
8385
+ /** The page kind: what `openTab` names and the guide entry opens. */
8386
+ const NATIVE_TAB_KIND = "danybg-theme";
8387
+ /**
8388
+ * Register the appearance pages with the native right Sidebar, if one exists.
8389
+ *
8390
+ * Call from the client `apply`. Both registrations sit inside the injected
8391
+ * scope's effects, so unloading the plugin (or HMR) withdraws them — without
8392
+ * that, the next activation would throw on the duplicate id.
8393
+ * @param ctx - client context.
8394
+ * @param opts.face - lazy accessor for the shared business face.
8395
+ * @param opts.store - the plugin's store instance, or null on a host where the
8396
+ * store module never resolved (in that case there is nothing to bind and no
8397
+ * page is registered).
8398
+ */
8399
+ function registerNativeSidebarTab(ctx, opts) {
8400
+ const { store } = opts;
8401
+ if (store === null || typeof store.getSnapshot !== "function" || typeof store.subscribe !== "function") {
8402
+ console.warn("dsh-any-background: no observable store instance on this host; the native sidebar page stays unregistered");
8403
+ return;
8404
+ }
8405
+ const locale = ctx.locale;
8406
+ const t = locale.bind(NS);
8407
+ const useStore = createStoreHook(store);
8408
+ ctx.inject?.(["sidebarRightTabs"], (scope) => {
8409
+ const tabs = scope.sidebarRightTabs;
8410
+ if (tabs === void 0 || typeof tabs.register !== "function") return;
8411
+ markNativeSidebarTabs();
8412
+ const face = opts.face();
8413
+ const ThemeBody = () => (0, react.createElement)(ThemeTab, {
8414
+ face,
8415
+ useStore,
8416
+ locale
8417
+ });
8418
+ scope.effect(() => {
8419
+ const yieldTo = () => suppressesBetterSidebar() === false && rBetterSidebar();
8420
+ if (yieldTo()) return;
8421
+ let disposeTab = null;
8422
+ const withdraw = () => {
8423
+ disposeTab?.();
8424
+ disposeTab = null;
8425
+ };
8426
+ const unsubscribeBetter = subscribe(() => {
8427
+ if (yieldTo()) withdraw();
8428
+ });
8429
+ const unsubscribeHost = subscribeHostInfo(() => {
8430
+ if (yieldTo()) withdraw();
8431
+ });
8432
+ disposeTab = tabs.register({
8433
+ id: NATIVE_TAB_ID,
8434
+ kind: NATIVE_TAB_KIND,
8435
+ title: () => t("nav"),
8436
+ guide: [{
8437
+ id: "theme",
8438
+ order: 55,
8439
+ title: () => t("nav"),
8440
+ description: () => t("guideDesc"),
8441
+ icon: SunIcon
8442
+ }]
8443
+ });
8444
+ return () => {
8445
+ unsubscribeBetter();
8446
+ unsubscribeHost();
8447
+ withdraw();
8448
+ };
8449
+ }, "dsh-any-background: native sidebar theme tab");
8450
+ scope.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
8451
+ name: "sidebar.right.pane.tab",
8452
+ key: NATIVE_TAB_ID
8453
+ }, ThemeBody)), "dsh-any-background: native sidebar theme body");
8454
+ });
8455
+ }
8456
+ //#endregion
8457
+ //#region src/client/header-tag.ts
8458
+ /**
8459
+ * Header-surface tagging.
8460
+ *
8461
+ * WHY A RUNTIME TAG IS NEEDED
8462
+ * The session-header dropdowns (open-in-app, session-log, Agent Team, the job
8463
+ * list, the subagent lineage tree) render their popover through the Menu/tree
8464
+ * primitives, which PORTAL the list to `document.body`. A portaled list is not a
8465
+ * DOM descendant of the header — no ancestor selector can reach it — and the
8466
+ * primitive emits no `id`/`aria-controls` back-link to its anchor either. Pure
8467
+ * CSS class matching was the remaining option, but 0.1.7 broke the shapes this
8468
+ * plugin relied on: open-in-app moved from in-place to `portal` (so the old
8469
+ * `:not(_portal)` discriminator no longer selected it), and the session row menu
8470
+ * became a dynamic `sidebar.workspaces.session.menu.item` slot whose rows are no
8471
+ * longer guaranteed to include a destructive one.
8472
+ *
8473
+ * THE SIGNAL
8474
+ * The slot renderer stamps every registered slot with `data-slot="<key>"`
8475
+ * (ui-renderer `scoped-slots.tsx`), and the session header's own slots are
8476
+ * `conversation.session.header{,.actions,.utilities,.corner,.lineage}` — present
8477
+ * on 0.1.5 through 0.1.7 alike. Those anchors live in the header subtree, so the
8478
+ * plugin can detect when a header trigger is expanded.
8479
+ *
8480
+ * WHY NOT GEOMETRY
8481
+ * A tempting link is the inline position the primitive writes on a portaled list
8482
+ * (`style="left: …; top: …"`). It was tried and REJECTED: an `align="end"` list
8483
+ * is flush to the trigger's right edge while the trigger itself sits at the far
8484
+ * right of the header, so the two boxes often do not overlap horizontally (in
8485
+ * the reported DOM the list is at `left:1012` with the trigger near x=1400). Any
8486
+ * purely geometric proximity test therefore misses real header menus.
8487
+ *
8488
+ * THE ACTUAL RULE (structural, no geometry, no timing)
8489
+ * A header dropdown can only be open while its trigger reports
8490
+ * `aria-expanded="true"`, and opening one is what mounts the popover. So a
8491
+ * popover tag is maintained as: "there is at least one expanded header trigger,
8492
+ * AND this popover is not owned by another group". Popovers are only ever
8493
+ * tagged/released on a change of that combined state, which makes the tag
8494
+ * self-consistent and idempotent.
8495
+ *
8496
+ * The one hazard is a popover that is open for an unrelated reason (a composer
8497
+ * menu, a hover card) *while* a header trigger stays expanded. Such a popover
8498
+ * is not a candidate: it is excluded by `ownsForeignSurface`, which skips any
8499
+ * popover carrying markers of a different slider group. In practice header
8500
+ * dropdowns are transient (opening one closes the others), so this is a guard
8501
+ * rather than the common path.
8502
+ *
8503
+ * @module
8504
+ */
8505
+ /** Marker attribute the stylesheet keys on. */
8506
+ const HEADER_POPOVER_ATTR = "data-dsh-any-header-popover";
8507
+ /** Slot anchors that identify the session header's own surfaces. */
8508
+ const HEADER_SLOT_SELECTOR = [
8509
+ "[data-slot=\"conversation.session.header\"]",
8510
+ "[data-slot=\"conversation.session.header.actions\"]",
8511
+ "[data-slot=\"conversation.session.header.utilities\"]",
8512
+ "[data-slot=\"conversation.session.header.corner\"]",
8513
+ "[data-slot=\"conversation.session.header.lineage\"]"
8514
+ ].join(",");
8515
+ /** Portaled popovers a header trigger can own. */
8516
+ const POPOVER_SELECTOR = "[role=\"menu\"], [role=\"tree\"], [role=\"dialog\"]";
8517
+ /** Whether any header slot anchor has an expanded trigger. */
8518
+ function headerTriggerOpen() {
8519
+ for (const anchor of document.querySelectorAll(HEADER_SLOT_SELECTOR)) if (anchor.querySelector("[aria-expanded=\"true\"]") !== null) return true;
8520
+ return false;
8521
+ }
8522
+ /** Surfaces that belong to a different slider group and must never be claimed.
8523
+ * `_denseList` without `_portal` is the in-place card-side dense menu, an
8524
+ * `_ioCard`/`_menu`-classed body-level list belongs elsewhere, and a modal is
8525
+ * its own surface. */
8526
+ function ownsForeignSurface(el) {
8527
+ if (el.getAttribute("aria-modal") === "true") return true;
8528
+ return el.parentElement !== document.body;
8529
+ }
8530
+ let observer = null;
8531
+ let raf = null;
8532
+ /** Recompute tags from the current open state. */
8533
+ function refreshHeaderPopovers() {
8534
+ const open = headerTriggerOpen();
8535
+ const candidates = [...document.querySelectorAll(POPOVER_SELECTOR)].filter((el) => !ownsForeignSurface(el));
8536
+ if (!open) {
8537
+ for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
8538
+ return;
8539
+ }
8540
+ for (const el of candidates) el.setAttribute(HEADER_POPOVER_ATTR, "");
8541
+ for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) if (!candidates.includes(el)) el.removeAttribute(HEADER_POPOVER_ATTR);
8542
+ }
8543
+ function schedule() {
8544
+ if (raf !== null) return;
8545
+ raf = window.requestAnimationFrame(() => {
8546
+ raf = null;
8547
+ refreshHeaderPopovers();
8548
+ });
8549
+ }
8550
+ /** Start tagging. Returns a teardown that stops observing and clears markings. */
8551
+ function startHeaderPopoverTagging() {
8552
+ if (typeof MutationObserver === "undefined" || typeof document === "undefined") return () => {};
8553
+ refreshHeaderPopovers();
8554
+ observer = new MutationObserver(schedule);
8555
+ observer.observe(document.body, {
8556
+ childList: true,
8557
+ subtree: true,
8558
+ attributes: true,
8559
+ attributeFilter: ["aria-expanded"]
8560
+ });
8561
+ return () => {
8562
+ observer?.disconnect();
8563
+ observer = null;
8564
+ if (raf !== null) {
8565
+ window.cancelAnimationFrame(raf);
8566
+ raf = null;
8567
+ }
8568
+ for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
8569
+ };
8570
+ }
8571
+ //#endregion
7600
8572
  //#region src/client/index.tsx
7601
8573
  /**
7602
8574
  * dsh-any-background — browser half entry.
@@ -7617,6 +8589,7 @@ window.__ModuleLoader__.load({
7617
8589
  initRpc((endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res));
7618
8590
  const [initH, initS, initL] = rColor();
7619
8591
  let customDispose = null;
8592
+ let registerFailed = false;
7620
8593
  const registerCustom = (h, s, l) => {
7621
8594
  customDispose?.();
7622
8595
  try {
@@ -7649,30 +8622,54 @@ window.__ModuleLoader__.load({
7649
8622
  colorScheme,
7650
8623
  tokens
7651
8624
  });
7652
- } catch {
8625
+ } catch (e) {
8626
+ if (!(() => {
8627
+ try {
8628
+ return ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
8629
+ } catch {
8630
+ return false;
8631
+ }
8632
+ })()) {
8633
+ if (!registerFailed) {
8634
+ registerFailed = true;
8635
+ console.error("dsh-any-background: host theme register failed; the custom skin will stay inactive", e);
8636
+ }
8637
+ customDispose = null;
8638
+ return false;
8639
+ }
7653
8640
  customDispose = null;
7654
8641
  }
7655
- const present = ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
8642
+ const present = (() => {
8643
+ try {
8644
+ return ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
8645
+ } catch {
8646
+ return false;
8647
+ }
8648
+ })();
7656
8649
  if (present) ctx.theme.setTheme(CUSTOM_ID);
7657
8650
  return present;
7658
8651
  };
7659
8652
  if (rHasColor()) registerCustom(initH, initS, initL);
7660
- onVerdictApplied(() => {
8653
+ const disposeVerdict = onVerdictApplied(() => {
7661
8654
  if (!rHasColor()) registerCustom();
7662
8655
  });
7663
- onColorAdopted((hsl) => {
8656
+ const disposeColorAdopted = onColorAdopted((hsl) => {
7664
8657
  registerCustom(hsl[0], hsl[1], hsl[2]);
7665
8658
  saveConfig();
7666
8659
  colorRev++;
7667
8660
  bound?.syncColor(hslToHsv(hsl[0], hsl[1], hsl[2]), colorRev);
7668
8661
  });
8662
+ ctx.effect(() => () => {
8663
+ disposeVerdict();
8664
+ disposeColorAdopted();
8665
+ }, "dsh-any-background: verdict/color listeners");
7669
8666
  ctx.effect(() => () => {
7670
8667
  customDispose?.();
7671
8668
  if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
7672
8669
  }, "dsh-any-background: skin dispose");
7673
8670
  const styleEl = document.createElement("style");
7674
8671
  styleEl.dataset.plugin = "dsh-any-background";
7675
- styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${POPOVER_BLUR_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}${MOBILE_HEADER_RULE}${PANEL_TOKEN_RULE}${PANEL_BLUR_RULE}${PRODUCED_RULE}${STROKE_RULE}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}`;
8672
+ styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${POPOVER_BLUR_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}${MOBILE_HEADER_RULE}${PANEL_TOKEN_RULE}${PANEL_PROMOTION_RULE}${PANEL_BLUR_RULE}${PRODUCED_RULE}${STROKE_RULE}${HEADER_POPOVER_RULE}${EXEMPT_DEFAULT_RULE}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}`;
7676
8673
  document.head.appendChild(styleEl);
7677
8674
  ctx.effect(() => () => {
7678
8675
  styleEl?.parentNode?.removeChild(styleEl);
@@ -7683,7 +8680,7 @@ window.__ModuleLoader__.load({
7683
8680
  let colorRev = 0;
7684
8681
  let bgRev = 0;
7685
8682
  const colorTimerRef = { current: null };
7686
- const store = defineStore({
8683
+ const storeSpec = defineStore === null ? null : defineStore({
7687
8684
  init: () => ({
7688
8685
  url: null,
7689
8686
  rev: -1,
@@ -7734,13 +8731,44 @@ window.__ModuleLoader__.load({
7734
8731
  }
7735
8732
  }
7736
8733
  });
8734
+ /** One shared instance, whatever the host build handed back.
8735
+ *
8736
+ * The plugin publishes a single appearance state that BOTH surfaces read —
8737
+ * the settings section (through the renderer's `useStore`) and the
8738
+ * better-sidebar page (through the hook in `sidebar/store-hook`). Handing the
8739
+ * host a declaration whose `create` always returns this very instance is what
8740
+ * keeps them from drifting: letting the renderer mint its own would give the
8741
+ * sidebar a second, permanently empty copy of the state. */
8742
+ const storeInstance = storeSpec === null ? null : typeof storeSpec.getSnapshot === "function" ? storeSpec : storeSpec.create();
8743
+ /** What gets registered: the instance on newer builds, a single-instance
8744
+ * declaration on the ones that expect a declaration. */
8745
+ const store = storeSpec === null || storeInstance === storeSpec ? storeSpec : {
8746
+ spec: storeSpec.spec ?? {},
8747
+ create: () => storeInstance
8748
+ };
7737
8749
  let bound = null;
8750
+ if (storeInstance !== null) bound = storeInstance.actions;
7738
8751
  const syncBg = () => {
7739
8752
  rev++;
7740
8753
  bgRev++;
7741
8754
  bound?.syncBg(rWp(), rev, cfg.backgroundType, cfg.generatedBg, bgRev, cfg.regenerateOnReload);
7742
8755
  };
7743
- onGeneratedSnapshot(syncBg);
8756
+ const disposeSnapshot = onGeneratedSnapshot(syncBg);
8757
+ ctx.effect(() => () => disposeSnapshot(), "dsh-any-background: snapshot listener");
8758
+ /** Capture the first frame of the video now applied and finish its
8759
+ * adaptation: the frame stands in for every still-image API (previews,
8760
+ * color extraction) and the brightness verdict follows it. The stale guard
8761
+ * skips a capture whose video was swapped out from under it (another
8762
+ * upload or a rotation switch landing mid-decode). */
8763
+ const captureAndApplyVideo = (url) => {
8764
+ if (url === null) return;
8765
+ captureVideoSnapshot(url).then((snap) => {
8766
+ if (rWpVideo() !== url) return;
8767
+ setWpVideoSnapshot(snap);
8768
+ applyThemeColor();
8769
+ syncBg();
8770
+ });
8771
+ };
7744
8772
  let metaRev = 0;
7745
8773
  const syncMetaNow = () => {
7746
8774
  metaRev++;
@@ -7861,7 +8889,7 @@ window.__ModuleLoader__.load({
7861
8889
  const scheduleTick = () => {
7862
8890
  const sc = rSchedule();
7863
8891
  if (!sc.enabled) return;
7864
- const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false : isNightNow(sc)) ? sc.nightProfile : sc.dayProfile;
8892
+ const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ?? false : isNightNow(sc)) ? sc.nightProfile : sc.dayProfile;
7865
8893
  if (!want || want === cfg.activeProfile) return;
7866
8894
  applyProfileById(want);
7867
8895
  };
@@ -7889,12 +8917,7 @@ window.__ModuleLoader__.load({
7889
8917
  if (cfg.backgroundType === "video") {
7890
8918
  const v = rWpVideo();
7891
8919
  if (v) {
7892
- captureVideoSnapshot(v).then((snap) => {
7893
- if (rWpVideo() !== v) return;
7894
- setWpVideoSnapshot(snap);
7895
- applyThemeColor();
7896
- syncBg();
7897
- });
8920
+ captureAndApplyVideo(v);
7898
8921
  applyWp();
7899
8922
  } else {
7900
8923
  cfg.backgroundType = "image";
@@ -7924,6 +8947,7 @@ window.__ModuleLoader__.load({
7924
8947
  teardownWp();
7925
8948
  }, "dsh-any-background: wp cleanup");
7926
8949
  ctx.effect(() => startBetterSidebarWatch(), "dsh-any-background: better-sidebar watch");
8950
+ ctx.effect(() => startHeaderPopoverTagging(), "dsh-any-background: header popover tagging");
7927
8951
  ctx.effect(() => ctx.on("theme/change", () => {
7928
8952
  if (rHasColor() || rBgDark() !== null || rSchemeOverride() !== "auto") {
7929
8953
  const snapshot = ctx.theme.getTheme();
@@ -7944,19 +8968,25 @@ window.__ModuleLoader__.load({
7944
8968
  document.body.append(sentinel);
7945
8969
  const viewportObserver = new ResizeObserver(applySoon);
7946
8970
  viewportObserver.observe(sentinel);
7947
- const dprQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
7948
- dprQuery.addEventListener("change", applySoon);
8971
+ const dprQuery = window.matchMedia?.(`(resolution: ${window.devicePixelRatio}dppx)`);
8972
+ dprQuery?.addEventListener?.("change", applySoon);
7949
8973
  ctx.effect(() => () => {
7950
8974
  viewportObserver.disconnect();
7951
- dprQuery.removeEventListener("change", applySoon);
8975
+ dprQuery?.removeEventListener?.("change", applySoon);
7952
8976
  sentinel.remove();
7953
8977
  }, "dsh-any-background: viewport watch");
7954
8978
  ctx.effect(() => ctx.locale.register(NS, {
7955
8979
  zh,
7956
8980
  en
7957
8981
  }), "dsh-any-background: i18n");
7958
- const sectionInject = (actions) => {
7959
- bound = actions;
8982
+ /** The business face, built once on first use and shared by both surfaces
8983
+ * (the settings section and the better-sidebar page). Memoizing is not just
8984
+ * about cost: the face closes over per-surface state — the font preview blob
8985
+ * URL behind releaseFontPreview — so a second build would give the sidebar
8986
+ * its own preview slot and leak whichever copy never gets released. */
8987
+ let themeFace = null;
8988
+ const buildFace = () => {
8989
+ if (themeFace !== null) return themeFace;
7960
8990
  syncBg();
7961
8991
  syncMetaNow();
7962
8992
  const playVideoFromBlob = (blob, mime) => {
@@ -7967,15 +8997,10 @@ window.__ModuleLoader__.load({
7967
8997
  setWpVideoUrl(localUrl, mime ?? blob.type ?? "video/mp4");
7968
8998
  applyWp();
7969
8999
  syncBg();
7970
- const applied = rWpVideo();
7971
- captureVideoSnapshot(localUrl).then((snap) => {
7972
- if (rWpVideo() !== applied) return;
7973
- setWpVideoSnapshot(snap);
7974
- applyThemeColor();
7975
- syncBg();
7976
- });
7977
- uploadVideo(blob, mime ?? blob.type ?? "video/mp4").then((ok) => {
7978
- if (ok) persistConfig();
9000
+ captureAndApplyVideo(rWpVideo());
9001
+ return uploadVideo(blob, mime ?? blob.type ?? "video/mp4").then((outcome) => {
9002
+ if (outcome.ok) persistConfig();
9003
+ return outcome;
7979
9004
  });
7980
9005
  };
7981
9006
  const FONT_EXT_MIME = {
@@ -8002,7 +9027,7 @@ window.__ModuleLoader__.load({
8002
9027
  };
8003
9028
  const [wh, ws, wl] = rColor();
8004
9029
  const [dh, ds, dv] = hslToHsv(wh, ws, wl);
8005
- return {
9030
+ const built = {
8006
9031
  t: ctx.locale.bind(NS),
8007
9032
  hue: dh,
8008
9033
  sat: ds,
@@ -8052,13 +9077,11 @@ window.__ModuleLoader__.load({
8052
9077
  applyThemeColor();
8053
9078
  syncBg();
8054
9079
  saveConfig();
8055
- return;
8056
- }
8057
- if (typeof u !== "string") {
8058
- playVideoFromBlob(u, mime);
8059
- return;
9080
+ return { ok: true };
8060
9081
  }
8061
- const live = await persistVideo(u) ? VIDEO_SERVE_URL : u;
9082
+ if (typeof u !== "string") return playVideoFromBlob(u, mime);
9083
+ const ok = await persistVideo(u);
9084
+ const live = ok ? VIDEO_SERVE_URL : u;
8062
9085
  cfg.backgroundType = "video";
8063
9086
  setWpUrl(null);
8064
9087
  cfg.videoBgState = { ...DEFAULT_CONFIG.bgState };
@@ -8066,13 +9089,8 @@ window.__ModuleLoader__.load({
8066
9089
  applyWp();
8067
9090
  saveConfig();
8068
9091
  syncBg();
8069
- const applied = rWpVideo();
8070
- captureVideoSnapshot(live).then((snap) => {
8071
- if (rWpVideo() !== applied) return;
8072
- setWpVideoSnapshot(snap);
8073
- applyThemeColor();
8074
- syncBg();
8075
- });
9092
+ captureAndApplyVideo(rWpVideo());
9093
+ return { ok };
8076
9094
  },
8077
9095
  setBgType: (type) => {
8078
9096
  setBackgroundType(type);
@@ -8116,18 +9134,21 @@ window.__ModuleLoader__.load({
8116
9134
  const localUrl = URL.createObjectURL(file);
8117
9135
  fontPreviewUrl = localUrl;
8118
9136
  applyFontFace(localUrl, true, fontMimeFromName(file.name));
8119
- const mime = await uploadFont(file);
8120
- if (mime === null) {
9137
+ const outcome = await uploadFont(file);
9138
+ if (!outcome.ok || outcome.mime === void 0) {
8121
9139
  applyStoredFont();
8122
9140
  releaseFontPreview(4e3);
8123
- return false;
9141
+ return {
9142
+ ok: false,
9143
+ refusal: outcome.refusal
9144
+ };
8124
9145
  }
8125
- cfg.fontMime = mime;
9146
+ cfg.fontMime = outcome.mime;
8126
9147
  cfg.fontEnabled = true;
8127
- applyFontFace(FONT_SERVE_URL, true, mime);
9148
+ applyFontFace(FONT_SERVE_URL, true, outcome.mime);
8128
9149
  releaseFontPreview(4e3);
8129
9150
  persistConfig();
8130
- return true;
9151
+ return { ok: true };
8131
9152
  },
8132
9153
  removeFont: () => {
8133
9154
  releaseFontPreview(0);
@@ -8213,7 +9234,7 @@ window.__ModuleLoader__.load({
8213
9234
  a.href = url;
8214
9235
  a.download = "dsh-any-theme.json";
8215
9236
  a.click();
8216
- URL.revokeObjectURL(url);
9237
+ window.setTimeout(() => URL.revokeObjectURL(url), 4e3);
8217
9238
  },
8218
9239
  importTheme: async (file) => {
8219
9240
  try {
@@ -8233,16 +9254,9 @@ window.__ModuleLoader__.load({
8233
9254
  }
8234
9255
  if (blob !== null) playVideoFromBlob(blob, cfg.videoMime);
8235
9256
  else {
8236
- const live = await persistVideo(video) ? VIDEO_SERVE_URL : video;
8237
- setWpVideoUrl(live, cfg.videoMime);
9257
+ setWpVideoUrl(await persistVideo(video) ? VIDEO_SERVE_URL : video, cfg.videoMime);
8238
9258
  applyWp();
8239
- const applied = rWpVideo();
8240
- captureVideoSnapshot(live).then((snap) => {
8241
- if (rWpVideo() !== applied) return;
8242
- setWpVideoSnapshot(snap);
8243
- applyThemeColor();
8244
- syncBg();
8245
- });
9259
+ captureAndApplyVideo(rWpVideo());
8246
9260
  }
8247
9261
  } else {
8248
9262
  setWpVideoUrl(null, null);
@@ -8401,18 +9415,19 @@ window.__ModuleLoader__.load({
8401
9415
  applyWp();
8402
9416
  saveConfig();
8403
9417
  syncBg();
8404
- const applied = rWpVideo();
8405
- captureVideoSnapshot(applied).then((snap) => {
8406
- if (rWpVideo() !== applied) return;
8407
- setWpVideoSnapshot(snap);
8408
- applyThemeColor();
8409
- syncBg();
8410
- });
9418
+ captureAndApplyVideo(rWpVideo());
8411
9419
  return true;
8412
9420
  }
8413
9421
  };
9422
+ themeFace = built;
9423
+ return built;
8414
9424
  };
8415
- ctx.slots.inject("settings.section", () => ctx.slots.register({
9425
+ const sectionInject = (actions) => {
9426
+ bound = actions;
9427
+ return buildFace();
9428
+ };
9429
+ if (store === null) console.error("dsh-any-background: settings panel disabled on this host (no store module)");
9430
+ else ctx.slots.inject("settings.section", () => ctx.slots.register({
8416
9431
  name: "settings.section",
8417
9432
  id: "dsh-any-background",
8418
9433
  order: 35,
@@ -8421,6 +9436,14 @@ window.__ModuleLoader__.load({
8421
9436
  store,
8422
9437
  inject: sectionInject
8423
9438
  }, ThemeSection));
9439
+ registerThemeSidebarTab(ctx, {
9440
+ face: buildFace,
9441
+ store: storeInstance
9442
+ });
9443
+ registerNativeSidebarTab(ctx, {
9444
+ face: buildFace,
9445
+ store: storeInstance
9446
+ });
8424
9447
  const navLabel = () => ctx.locale.bind(NS)("nav");
8425
9448
  const applyNavIcon = () => {
8426
9449
  const nav = document.querySelector("[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]")?.querySelector("nav");