dsh-background-by-model 0.5.2 → 0.5.4

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
@@ -78,7 +78,9 @@ window.__ModuleLoader__.load({
78
78
  ruleEmptyHint: "这条规则还没有图片,不会被匹配到,也不会作为兜底",
79
79
  ruleColor: "主题色",
80
80
  ruleColorNone: "系统主题",
81
- ruleColorNoneHint: "未设颜色时跟随系统主题",
81
+ ruleColorNoneHint: "未设颜色时跟随系统主题:界面用宿主的配色,壁纸与各部位的透明度、模糊照旧生效",
82
+ ruleColorPick: "自定义颜色",
83
+ ruleColorPickHint: "优先从本图取色,取不到就用默认色",
82
84
  ruleColorExtract: "从本图提取",
83
85
  ruleLayout: "布局模式",
84
86
  ruleFraming: "取景",
@@ -183,7 +185,9 @@ window.__ModuleLoader__.load({
183
185
  ruleEmptyHint: "This rule has no image — it can neither match nor serve as the fallback",
184
186
  ruleColor: "Theme color",
185
187
  ruleColorNone: "System theme",
186
- ruleColorNoneHint: "Without a color this rule follows the system theme",
188
+ ruleColorNoneHint: "Without a color this rule follows the system theme: the host palette paints the interface while the wallpaper and every opacity/blur slider keep working",
189
+ ruleColorPick: "Custom color",
190
+ ruleColorPickHint: "Extracts from this image when it can, otherwise uses a default color",
187
191
  ruleColorExtract: "Extract from image",
188
192
  ruleLayout: "Layout mode",
189
193
  ruleFraming: "Framing",
@@ -727,6 +731,7 @@ window.__ModuleLoader__.load({
727
731
  const l = (d) => Math.max(0, Math.min(1, lit + d));
728
732
  const hsl = (hh, ss, ll) => `hsl(${Math.round(hh)},${Math.round(ss * 100)}%,${Math.round(ll * 100)}%)`;
729
733
  const rgba = (hh, ss, ll, a) => `hsla(${Math.round(hh)},${Math.round(ss * 100)}%,${Math.round(ll * 100)}%,${a})`;
734
+ const onBrand = l(.2) > .6 ? "#000" : "#fff";
730
735
  if (dark) {
731
736
  const menuFill = hsl(h(0), s(0), l(.08));
732
737
  return {
@@ -752,11 +757,11 @@ window.__ModuleLoader__.load({
752
757
  "--dsw-alias-label-dimmed": "rgba(255,255,255,0.35)",
753
758
  "--dsw-alias-label-quaternary": "rgba(255,255,255,0.25)",
754
759
  "--dsw-alias-label-primary-dimmed": "rgba(255,255,255,0.92)",
755
- "--dsw-alias-label-primary-foreground": hsl(0, 0, 1),
760
+ "--dsw-alias-label-primary-foreground": onBrand,
756
761
  "--dsw-alias-label-primary-inverted": hsl(h(0), s(.08), Math.min(l(.06), .16)),
757
762
  "--dsw-alias-label-primary-bluish": hsl(0, 0, 1),
758
763
  "--dsw-alias-brand-primary": hsl(h(0), s(.1), Math.max(l(.2), .5)),
759
- "--dsw-alias-brand-text": l(.2) > .6 ? "#000" : "#fff",
764
+ "--dsw-alias-brand-text": onBrand,
760
765
  "--dsw-alias-button-primary-fill": hsl(h(0), s(.1), Math.max(l(.2), .5)),
761
766
  "--dsw-alias-button-primary-hover": hsl(h(0), s(.1), Math.max(l(.28), .58)),
762
767
  "--dsw-alias-button-primary-dimmed": hsl(h(0), s(0), l(.07)),
@@ -832,6 +837,7 @@ window.__ModuleLoader__.load({
832
837
  "--dsw-alias-label-caption": "rgba(0,0,0,0.5)",
833
838
  "--dsw-alias-label-dimmed": "rgba(0,0,0,0.35)",
834
839
  "--dsw-alias-label-quaternary": "rgba(0,0,0,0.25)",
840
+ "--dsw-alias-label-primary-inverted": "#fff",
835
841
  "--dsw-alias-brand-primary": hsl(h(0), s(.05), Math.min(l(-.18), .45)),
836
842
  "--dsw-alias-brand-text": "#fff",
837
843
  "--dsw-alias-button-primary-hover": hsl(h(0), s(.05), Math.min(l(-.12), .5)),
@@ -1067,7 +1073,7 @@ window.__ModuleLoader__.load({
1067
1073
  .dab-shell{display:grid;grid-template-columns:158px minmax(0,1fr);gap:26px;align-items:start;padding-bottom:8px;width:100%;max-width:980px;margin:0 auto}
1068
1074
  .dab-nav{position:sticky;top:0;display:flex;flex-direction:column;gap:18px}
1069
1075
  .dab-brand{display:flex;align-items:center;gap:10px;padding:2px 6px}
1070
- .dab-brand-tile{width:30px;height:30px;flex:none;border-radius:9px;display:grid;place-items:center;color:var(--dsw-alias-brand-text);background:var(--dsw-alias-brand-primary);box-shadow:0 4px 14px -4px var(--dsw-alias-brand-primary)}
1076
+ .dab-brand-tile{width:30px;height:30px;flex:none;border-radius:9px;display:grid;place-items:center;color:var(--dsw-alias-label-primary-inverted);background:var(--dsw-alias-brand-primary);box-shadow:0 4px 14px -4px var(--dsw-alias-brand-primary)}
1071
1077
  .dab-brand-name{font-size:13px;font-weight:650;letter-spacing:.01em;line-height:1.25}
1072
1078
  .dab-brand-tag{font-size:9px;letter-spacing:.16em;font-weight:600;color:var(--dsw-alias-label-quaternary,var(--dsw-alias-label-tertiary));text-transform:uppercase}
1073
1079
  .dab-nav-list{position:relative;display:flex;flex-direction:column;gap:4px}
@@ -1127,11 +1133,23 @@ window.__ModuleLoader__.load({
1127
1133
  .dab-swatch-lg:hover{transform:scale(1.02)}
1128
1134
 
1129
1135
  /* ── buttons & chips ─────────────────────────────────────────────────────── */
1136
+ /* Text/icon painted ON --dsw-alias-brand-primary reads
1137
+ * --dsw-alias-label-primary-inverted, never --dsw-alias-brand-text. The host's
1138
+ * brand-primary is a CONTRAST ink, not a hue: black in its light scheme and
1139
+ * near-white (#f9fafb) in its dark one, with brand-text equal to it in BOTH
1140
+ * (and unused by the host's own CSS). Following the system theme therefore made
1141
+ * every brand-filled control a filled box with an invisible label — a white
1142
+ * primary button and a white active chip on a dark dialog. It was only ever
1143
+ * readable while the plugin owned the palette and generated its own brand pair.
1144
+ * label-primary-inverted is the host's answer for ink on a brand/contrast fill
1145
+ * (its sidebar build badge paints exactly this pair) and it flips with the
1146
+ * scheme: #fff on the light scheme's black fill, #353638 on the dark scheme's
1147
+ * white one. */
1130
1148
  .dab-btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:34px;padding:0 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:550;cursor:pointer;transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease,opacity .18s ease,background .18s ease}
1131
1149
  .dab-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 5px 14px -6px rgba(0,0,0,.32)}
1132
1150
  .dab-btn:active:not(:disabled){transform:translateY(0) scale(.97);box-shadow:none}
1133
1151
  .dab-btn:disabled{opacity:.5;cursor:not-allowed}
1134
- .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);border-color:transparent}
1152
+ .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted);border-color:transparent}
1135
1153
  .dab-btn-danger{color:var(--dsw-alias-state-error-primary)}
1136
1154
  .dab-btn-ghost{background:transparent;border-color:transparent;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
1137
1155
  .dab-btn-ghost:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);box-shadow:none}
@@ -1139,7 +1157,7 @@ window.__ModuleLoader__.load({
1139
1157
  .dab-chip-row{display:flex;flex-wrap:wrap;gap:8px}
1140
1158
  .dab-chip{height:30px;padding:0 14px;border-radius:99px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .22s ease}
1141
1159
  .dab-chip:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
1142
- .dab-chip.is-active{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);border-color:transparent}
1160
+ .dab-chip.is-active{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted);border-color:transparent}
1143
1161
 
1144
1162
  /* ── sliders ─────────────────────────────────────────────────────────────── */
1145
1163
  .dab-slider-block{display:flex;flex-direction:column;gap:6px}
@@ -1170,7 +1188,7 @@ window.__ModuleLoader__.load({
1170
1188
  .dab-toggle{position:relative;width:42px;height:24px;flex:none;border-radius:99px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;padding:0;transition:background .28s ease,border-color .28s ease}
1171
1189
  .dab-toggle-knob{position:absolute;top:2.5px;left:2.5px;width:17px;height:17px;border-radius:50%;background:var(--dsw-alias-label-secondary,#999);box-shadow:0 1px 3px rgba(0,0,0,.3);transition:transform .28s cubic-bezier(.22,1,.36,1),background .28s ease}
1172
1190
  .dab-toggle.is-on{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
1173
- .dab-toggle.is-on .dab-toggle-knob{transform:translateX(18px);background:#fff}
1191
+ .dab-toggle.is-on .dab-toggle-knob{transform:translateX(18px);background:var(--dsw-alias-label-primary-inverted)}
1174
1192
 
1175
1193
  /* ── background preview hero ─────────────────────────────────────────────── */
1176
1194
  .dab-hero{position:relative;border-radius:16px;overflow:hidden;aspect-ratio:16/9;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)}
@@ -1208,7 +1226,7 @@ window.__ModuleLoader__.load({
1208
1226
  .dab-overlay-hint{color:rgba(255,255,255,.62);font-size:12px}
1209
1227
  .dab-modal-card{animation:dab-zoom-in .3s cubic-bezier(.22,1,.36,1) both;max-width:calc(100vw - 40px);max-height:calc(100vh - 120px);overflow:auto}
1210
1228
  .dab-overlay .dab-btn{background:rgba(255,255,255,.94);color:#14161a;border-color:transparent}
1211
- .dab-overlay .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text)}
1229
+ .dab-overlay .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted)}
1212
1230
  .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}
1213
1231
  .dab-crash-title{font-size:15px;font-weight:650}
1214
1232
  .dab-crash-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.5}
@@ -1341,6 +1359,24 @@ window.__ModuleLoader__.load({
1341
1359
  }
1342
1360
  //#endregion
1343
1361
  //#region src/client/wallpaper.ts
1362
+ /**
1363
+ * `data-ds-dark-theme` value the plugin writes while it forces its own dark
1364
+ * palette. The host treats the attribute as a boolean (`toggleAttribute`), so a
1365
+ * plugin-specific value still matches every `body[data-ds-dark-theme]` rule
1366
+ * while staying distinguishable from the host's own write — which is what lets
1367
+ * `releaseOwnScheme` hand the flag back without clobbering the host's scheme.
1368
+ */
1369
+ const OWN_SCHEME = "dsh-background-by-model";
1370
+ const DARK_ATTR = "data-ds-dark-theme";
1371
+ /**
1372
+ * Marker on <html> while the plugin owns a palette. The dialog/trajectory rules
1373
+ * below redirect NO-fallback tokens (`--dsw-alias-bg-layer-*`) to plugin-owned
1374
+ * variables, so they may only match while those variables are actually written:
1375
+ * without the gate a palette-less state would leave them reading a
1376
+ * guaranteed-invalid value (transparent panels) instead of handing the surface
1377
+ * back to the host untouched.
1378
+ */
1379
+ const THEMED_ATTR = "data-dab-themed";
1344
1380
  let wpEl = null;
1345
1381
  let appliedTokenNames = [];
1346
1382
  let tokenStyleEl = null;
@@ -1356,6 +1392,22 @@ window.__ModuleLoader__.load({
1356
1392
  if (tokenStyleEl) tokenStyleEl.textContent = "";
1357
1393
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1358
1394
  appliedTokenNames = [];
1395
+ baseTokenKey = "";
1396
+ lastBgKey = "";
1397
+ }
1398
+ /** Drop every surface the plugin owns: the variables the scoped
1399
+ * dialog/trajectory rules read, and the inline backgrounds written onto the
1400
+ * AppFrame columns. Used when no palette is available and at teardown — without
1401
+ * it the last color's surfaces outlive the color that produced them. */
1402
+ function clearOwnedSurfaces() {
1403
+ discoverParts();
1404
+ const root = document.documentElement;
1405
+ for (const name of OWNED_SURFACE_VARS) root.style.removeProperty(name);
1406
+ for (const el of [
1407
+ frameEl,
1408
+ centerEl,
1409
+ detailsEl
1410
+ ]) el?.style.removeProperty("background");
1359
1411
  }
1360
1412
  const OPACITY_TOKEN_GROUPS = [
1361
1413
  {
@@ -1392,6 +1444,142 @@ window.__ModuleLoader__.load({
1392
1444
  "--dsw-menu-surface-fill": "--dsh-any-op-menu"
1393
1445
  };
1394
1446
  let baseTokenKey = "";
1447
+ const HOST_SURFACE_TOKENS = Object.keys(OPACITY_VARS);
1448
+ /** Plugin-owned variables the scoped dialog/trajectory rules read. */
1449
+ const SETTINGS_SURFACE_VARS = [
1450
+ "--dsh-any-bg-settings-surface",
1451
+ "--dsh-any-bg-settings-layer-1",
1452
+ "--dsh-any-bg-settings-layer-2",
1453
+ "--dsh-any-bg-settings-layer-3"
1454
+ ];
1455
+ const TRAJECTORY_SURFACE_VARS = [
1456
+ "--dsh-any-traj-layer-1",
1457
+ "--dsh-any-traj-layer-2",
1458
+ "--dsh-any-traj-layer-3"
1459
+ ];
1460
+ const OWNED_SURFACE_VARS = [...SETTINGS_SURFACE_VARS, ...TRAJECTORY_SURFACE_VARS];
1461
+ let hostProbe = null;
1462
+ /** Resolve any color the engine understands (hex, rgb()/hsl(), a var() chain,
1463
+ * color-mix(), …) to its USED value: only a used color can be re-emitted with
1464
+ * an alpha. Returns null when the engine rejects the value. */
1465
+ function resolveCssColor(raw) {
1466
+ const value = raw.trim();
1467
+ if (value === "") return null;
1468
+ if (hostProbe === null) {
1469
+ hostProbe = document.createElement("span");
1470
+ hostProbe.setAttribute("aria-hidden", "true");
1471
+ hostProbe.style.cssText = "position:absolute;width:0;height:0;pointer-events:none";
1472
+ }
1473
+ if (!hostProbe.isConnected) document.body.appendChild(hostProbe);
1474
+ hostProbe.style.color = "";
1475
+ hostProbe.style.color = value;
1476
+ if (hostProbe.style.color === "") return null;
1477
+ return getComputedStyle(hostProbe).color || null;
1478
+ }
1479
+ /** Read the host's own surface colors out of the live cascade. */
1480
+ function readHostPalette() {
1481
+ const sheet = tokenStyleEl;
1482
+ const muted = sheet !== null && sheet.isConnected && !sheet.disabled;
1483
+ if (muted) sheet.disabled = true;
1484
+ try {
1485
+ const computed = getComputedStyle(document.body);
1486
+ const tokens = {};
1487
+ for (const name of HOST_SURFACE_TOKENS) {
1488
+ const used = resolveCssColor(computed.getPropertyValue(name));
1489
+ if (used !== null) tokens[name] = used;
1490
+ }
1491
+ return tokens["--dsw-alias-bg-base"] === void 0 ? null : {
1492
+ own: false,
1493
+ tokens
1494
+ };
1495
+ } finally {
1496
+ if (muted) sheet.disabled = false;
1497
+ }
1498
+ }
1499
+ let hostPaletteCache = null;
1500
+ const HOST_PALETTE_TTL_MS = 1e3;
1501
+ function invalidateHostPalette() {
1502
+ hostPaletteCache = null;
1503
+ }
1504
+ function hostPalette() {
1505
+ const now = Date.now();
1506
+ if (hostPaletteCache !== null && now - hostPaletteCache.at < HOST_PALETTE_TTL_MS) return hostPaletteCache.palette;
1507
+ const read = readHostPalette();
1508
+ hostPaletteCache = {
1509
+ at: now,
1510
+ palette: read
1511
+ };
1512
+ return read;
1513
+ }
1514
+ /** The palette every surface is currently painted from. */
1515
+ function palette() {
1516
+ if (!rHasColor()) return hostPalette();
1517
+ const [h, s, l] = rColor();
1518
+ return {
1519
+ own: true,
1520
+ tokens: genTokens(h, s, l).tokens
1521
+ };
1522
+ }
1523
+ /** A resolved color with a new alpha. The plugin's own tokens are `hsl()` and
1524
+ * the host's alias tokens resolve to hex, both of which `toRgba` parses; a host
1525
+ * publishing an already-composited value (`color(…)`, `oklab(…)`, …) cannot be
1526
+ * parsed there, and emitting it unchanged would paint an OPAQUE surface — the
1527
+ * one outcome the transparent-surface design must never fall back to. Those go
1528
+ * through `color-mix` instead. */
1529
+ function withAlpha(color, alpha) {
1530
+ const converted = toRgba(color, alpha);
1531
+ if (/^(rgb|hsl)a?\(/i.test(converted)) return converted;
1532
+ return `color-mix(in srgb, ${color} ${Math.round(Math.max(0, Math.min(1, alpha)) * 100)}%, transparent)`;
1533
+ }
1534
+ /**
1535
+ * The color scheme the HOST's own preference resolves to, pushed in by the
1536
+ * browser half (the theme service knows it, custom themes included). `null`
1537
+ * means "unknown", which is the only case where the plugin falls back to the
1538
+ * preference the host's boot script published and to the OS query itself.
1539
+ */
1540
+ let hostSchemeDark = null;
1541
+ function setHostScheme(dark) {
1542
+ hostSchemeDark = dark;
1543
+ }
1544
+ function resolveHostDark() {
1545
+ if (typeof hostSchemeDark === "boolean") return hostSchemeDark;
1546
+ const source = document.documentElement.dataset.dsThemeSource;
1547
+ if (source === "dark") return true;
1548
+ if (source === "light") return false;
1549
+ try {
1550
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
1551
+ } catch {
1552
+ return false;
1553
+ }
1554
+ }
1555
+ /**
1556
+ * Keep `data-ds-dark-theme` in the HOST's own form (a boolean attribute) while
1557
+ * the plugin does not own a palette.
1558
+ *
1559
+ * The plugin overwrites that flag whenever it owns a palette — including
1560
+ * REMOVING it to force a light one — and the host only re-projects the flag on a
1561
+ * theme change. So a flag the plugin dropped can stick: the host keeps painting
1562
+ * its light palette on a dark system, the readback then hands those light
1563
+ * surfaces back to the plugin, and the interface stays light until some
1564
+ * unrelated event happens to re-run an apply. Restoring the flag from the
1565
+ * resolved scheme closes that hole, and the write is skipped when it already
1566
+ * matches so this never churns the theme observer.
1567
+ */
1568
+ function adoptHostScheme() {
1569
+ const dark = resolveHostDark();
1570
+ const current = document.body.getAttribute(DARK_ATTR);
1571
+ if (dark) {
1572
+ if (current !== "") document.body.setAttribute(DARK_ATTR, "");
1573
+ } else if (current !== null) document.body.removeAttribute(DARK_ATTR);
1574
+ }
1575
+ /** Re-read the host palette and repaint from it when it moved. Called while a
1576
+ * color-less rule is active (the host can re-project its palette at any time,
1577
+ * with no event of ours to hang a repaint on). */
1578
+ function refreshSystemTheme() {
1579
+ adoptHostScheme();
1580
+ invalidateHostPalette();
1581
+ applyCustomTokens(rOps());
1582
+ }
1395
1583
  let pendingOps = null;
1396
1584
  let tokensRaf = null;
1397
1585
  function applyCustomTokens(ops) {
@@ -1407,26 +1595,38 @@ window.__ModuleLoader__.load({
1407
1595
  }
1408
1596
  let lastBgKey = "";
1409
1597
  function applyCustomTokensNow(ops) {
1598
+ const src = palette();
1599
+ if (src === null) {
1600
+ clearCustomTokens();
1601
+ clearOwnedSurfaces();
1602
+ adoptHostScheme();
1603
+ return;
1604
+ }
1605
+ const { own, tokens } = src;
1410
1606
  const [h, s, l] = rColor();
1411
- const { tokens } = genTokens(h, s, l);
1412
1607
  try {
1413
- const forceDark = l < .55;
1414
- if (`${h}|${s}|${l}` !== baseTokenKey) {
1415
- baseTokenKey = `${h}|${s}|${l}`;
1416
- if (forceDark) document.body.setAttribute("data-ds-dark-theme", "dsh-background-by-model");
1417
- else document.body.removeAttribute("data-ds-dark-theme");
1418
- const decls = [`color-scheme:${forceDark ? "dark" : "light"}`];
1608
+ const forceDark = own && l < .55;
1609
+ const key = own ? `own:${h}|${s}|${l}` : `host:${HOST_SURFACE_TOKENS.map((n) => tokens[n] ?? "").join("|")}`;
1610
+ if (key !== baseTokenKey) {
1611
+ if (own) {
1612
+ if (forceDark) document.body.setAttribute(DARK_ATTR, OWN_SCHEME);
1613
+ else document.body.removeAttribute(DARK_ATTR);
1614
+ } else adoptHostScheme();
1615
+ const decls = own ? [`color-scheme:${forceDark ? "dark" : "light"}`] : [];
1419
1616
  for (const [name, value] of Object.entries(tokens)) {
1420
1617
  const opVar = OPACITY_VARS[name];
1421
1618
  decls.push(`${name}:${opVar !== void 0 ? `var(${opVar})` : value}!important`);
1422
1619
  }
1423
1620
  ensureTokenStyle().textContent = `body{${decls.join(";")}}`;
1424
- for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1425
- appliedTokenNames = Object.keys(tokens);
1621
+ if (own) {
1622
+ for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1623
+ appliedTokenNames = Object.keys(tokens);
1624
+ }
1625
+ baseTokenKey = key;
1426
1626
  }
1427
1627
  const root = document.documentElement;
1428
- for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) root.style.setProperty(OPACITY_VARS[name], toRgba(tokens[name] ?? "#000", ops[g.part]));
1429
- root.style.setProperty("--dsh-any-op-menu-cordis", toRgba(tokens["--dsw-specific-menu"] ?? "#000", ops.input));
1628
+ for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) root.style.setProperty(OPACITY_VARS[name], withAlpha(tokens[name] ?? "#000", ops[g.part]));
1629
+ root.style.setProperty("--dsh-any-op-menu-cordis", withAlpha(tokens["--dsw-specific-menu"] ?? "#000", ops.input));
1430
1630
  const bgKey = `${baseTokenKey}|${ops.bg}`;
1431
1631
  if (bgKey !== lastBgKey) {
1432
1632
  lastBgKey = bgKey;
@@ -1435,45 +1635,48 @@ window.__ModuleLoader__.load({
1435
1635
  } catch {}
1436
1636
  }
1437
1637
  const SETTINGS_PANEL_SEL = "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]";
1438
- const SETTINGS_STYLE_RULE = `${SETTINGS_PANEL_SEL}{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));backdrop-filter:var(--dsh-any-blur-settings,none);--dsw-alias-bg-layer-1:var(--dsh-any-bg-settings-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-bg-settings-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-bg-settings-layer-3)}${SETTINGS_PANEL_SEL} .dab-card{backdrop-filter:var(--dsh-any-blur-card-panels,none);-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,none)}`;
1638
+ const SETTINGS_STYLE_RULE = `${SETTINGS_PANEL_SEL}{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));backdrop-filter:var(--dsh-any-blur-settings,none);}html[${THEMED_ATTR}] ${SETTINGS_PANEL_SEL}{--dsw-alias-bg-layer-1:var(--dsh-any-bg-settings-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-bg-settings-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-bg-settings-layer-3)}${SETTINGS_PANEL_SEL} .dab-card{backdrop-filter:var(--dsh-any-blur-card-panels,none);-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,none)}`;
1439
1639
  const INPUT_BLUR_RULE = "[data-composer-card],[data-cordis-panel]{-webkit-backdrop-filter:var(--dsh-any-input-blur,none);backdrop-filter:var(--dsh-any-input-blur,none)}[data-cordis-panel]{--dsw-specific-menu:var(--dsh-any-op-menu-cordis)!important}";
1440
1640
  function applyInputBlur(px) {
1441
1641
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-input-blur", `blur(${px}px)`);
1442
1642
  else document.documentElement.style.removeProperty("--dsh-any-input-blur");
1443
1643
  }
1444
1644
  function applySettingsOverrides(op) {
1445
- const [h, s, l] = rColor();
1446
- const tokens = genTokens(h, s, l).tokens;
1447
- const layer1 = tokens["--dsw-alias-bg-layer-1"];
1448
- const layer2 = tokens["--dsw-alias-bg-layer-2"];
1449
- const layer3 = tokens["--dsw-alias-bg-layer-3"];
1450
- if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-surface", toRgba(layer2, op));
1451
- if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-1", toRgba(layer1, op));
1452
- if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-2", toRgba(layer2, op));
1453
- if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-3", toRgba(layer3, op));
1454
- }
1455
- 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)}";
1645
+ const tokens = palette()?.tokens ?? {};
1646
+ const root = document.documentElement;
1647
+ const write = (name, token) => {
1648
+ const value = tokens[token];
1649
+ if (value === void 0) root.style.removeProperty(name);
1650
+ else root.style.setProperty(name, withAlpha(value, op));
1651
+ };
1652
+ write("--dsh-any-bg-settings-surface", "--dsw-alias-bg-layer-2");
1653
+ write("--dsh-any-bg-settings-layer-1", "--dsw-alias-bg-layer-1");
1654
+ write("--dsh-any-bg-settings-layer-2", "--dsw-alias-bg-layer-2");
1655
+ write("--dsh-any-bg-settings-layer-3", "--dsw-alias-bg-layer-3");
1656
+ }
1657
+ const TRAJECTORY_STYLE_RULE = `html[${THEMED_ATTR}] [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)}`;
1456
1658
  function applyTrajectoryOverrides(op) {
1457
- const [h, s, l] = rColor();
1458
- const tokens = genTokens(h, s, l).tokens;
1459
- const layer1 = tokens["--dsw-alias-bg-layer-1"];
1460
- const layer2 = tokens["--dsw-alias-bg-layer-2"];
1461
- const layer3 = tokens["--dsw-alias-bg-layer-3"];
1462
- if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-1", toRgba(layer1, op));
1463
- if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-2", toRgba(layer2, op));
1464
- if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-3", toRgba(layer3, op));
1659
+ const tokens = palette()?.tokens ?? {};
1660
+ const root = document.documentElement;
1661
+ const write = (name, token) => {
1662
+ const value = tokens[token];
1663
+ if (value === void 0) root.style.removeProperty(name);
1664
+ else root.style.setProperty(name, withAlpha(value, op));
1665
+ };
1666
+ write("--dsh-any-traj-layer-1", "--dsw-alias-bg-layer-1");
1667
+ write("--dsh-any-traj-layer-2", "--dsw-alias-bg-layer-2");
1668
+ write("--dsh-any-traj-layer-3", "--dsw-alias-bg-layer-3");
1465
1669
  }
1466
1670
  const RIGHTBAR_STYLE_RULE = "div[data-sidebar-right-panel]:not(:has([data-dockkit-content],[data-dockkit-empty]))[data-sidebar-right-open],div[data-sidebar-right-panel] [data-dockkit-content]:not([data-dockkit-float]),div[data-sidebar-right-panel] [data-dockkit-empty]{background:var(--dsh-any-bg-rightbar,var(--dsw-alias-bg-base))!important}div[data-sidebar-right-panel][data-sidebar-right-open]{-webkit-backdrop-filter:var(--dsh-any-blur-rightbar,none);backdrop-filter:var(--dsh-any-blur-rightbar,none)}";
1467
1671
  /** Paint the file-preview panel's surface from its own slider. */
1468
1672
  function applyRightbarOverrides(op) {
1469
1673
  const root = document.documentElement;
1470
- if (!rHasColor()) {
1674
+ const base = palette()?.tokens["--dsw-alias-bg-base"];
1675
+ if (base === void 0) {
1471
1676
  root.style.removeProperty("--dsh-any-bg-rightbar");
1472
1677
  return;
1473
1678
  }
1474
- const [h, s, l] = rColor();
1475
- const base = genTokens(h, s, l).tokens["--dsw-alias-bg-base"];
1476
- if (base !== void 0) root.style.setProperty("--dsh-any-bg-rightbar", toRgba(base, op));
1679
+ root.style.setProperty("--dsh-any-bg-rightbar", withAlpha(base, op));
1477
1680
  }
1478
1681
  /** Extra backdrop frost for the file-preview panel (0 = no frost of its own). */
1479
1682
  function applyRightbarBlur(px) {
@@ -1542,14 +1745,20 @@ window.__ModuleLoader__.load({
1542
1745
  * reducing the main-bg opacity stacked a second alpha onto the sidebar; moving
1543
1746
  * the alpha onto the columns keeps the sidebar owned by its own slider. */
1544
1747
  function applyPartOpacities(ops) {
1545
- if (!rHasColor()) return;
1546
1748
  discoverParts();
1547
1749
  if (frameEl === null) return;
1548
- const [h, s, l] = rColor();
1549
- const base = genTokens(h, s, l).tokens["--dsw-alias-bg-base"];
1750
+ const base = palette()?.tokens["--dsw-alias-bg-base"];
1751
+ if (base === void 0) {
1752
+ for (const el of [
1753
+ frameEl,
1754
+ centerEl,
1755
+ detailsEl
1756
+ ]) el?.style.removeProperty("background");
1757
+ return;
1758
+ }
1550
1759
  frameEl.style.background = "transparent";
1551
- if (centerEl !== null) centerEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
1552
- if (detailsEl !== null) detailsEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
1760
+ if (centerEl !== null) centerEl.style.background = withAlpha(base, ops.bg);
1761
+ if (detailsEl !== null) detailsEl.style.background = withAlpha(base, ops.bg);
1553
1762
  }
1554
1763
  /** Blur of the option panels inside the settings dialog (.dab-card), owned by
1555
1764
  * the "dialog option panel" (card) blur slider. Written as a plugin-owned
@@ -1762,8 +1971,7 @@ window.__ModuleLoader__.load({
1762
1971
  function applyViewCards() {
1763
1972
  discoverParts();
1764
1973
  if (centerEl === null) return;
1765
- const [h, s, l] = rColor();
1766
- const surface = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-1"];
1974
+ const surface = palette()?.tokens["--dsw-alias-bg-layer-1"];
1767
1975
  VIEW_CARDS.forEach((spec, i) => {
1768
1976
  const target = discoverViewTarget(i, spec);
1769
1977
  if (target === null) return;
@@ -1773,8 +1981,8 @@ window.__ModuleLoader__.load({
1773
1981
  if (!plain) {
1774
1982
  if (!target.hasAttribute(spec.mark)) stashCardPrev(target, spec.prev, false);
1775
1983
  const borderAlpha = opacity > 0 ? Math.min(1, opacity * 1.5) : blurPx > 0 ? .35 : 0;
1776
- target.style.background = surface !== void 0 ? toRgba(surface, opacity) : "transparent";
1777
- target.style.border = surface !== void 0 ? `1px solid ${toRgba(surface, borderAlpha)}` : "1px solid transparent";
1984
+ target.style.background = surface !== void 0 ? withAlpha(surface, opacity) : "transparent";
1985
+ target.style.border = surface !== void 0 ? `1px solid ${withAlpha(surface, borderAlpha)}` : "1px solid transparent";
1778
1986
  target.style.borderRadius = "16px";
1779
1987
  target.style.padding = "18px";
1780
1988
  }
@@ -1806,31 +2014,38 @@ window.__ModuleLoader__.load({
1806
2014
  let themeRaf = 0;
1807
2015
  function reassertScheme() {
1808
2016
  const [, , l] = rColor();
1809
- if (l < .55) document.body.setAttribute("data-ds-dark-theme", "dsh-background-by-model");
1810
- else document.body.removeAttribute("data-ds-dark-theme");
2017
+ if (l < .55) document.body.setAttribute(DARK_ATTR, OWN_SCHEME);
2018
+ else document.body.removeAttribute(DARK_ATTR);
1811
2019
  applyCustomTokens(rOps());
1812
2020
  }
1813
2021
  /** Re-assert the plugin's forced scheme whenever the host strips it, so a
1814
- * refresh / cold-load / set-change never flashes a light frame. */
2022
+ * refresh / cold-load / set-change never flashes a light frame. Also the point
2023
+ * where a host-side scheme flip is noticed while a color-less rule runs: that
2024
+ * rule paints from the host palette, so the readback is dropped and the
2025
+ * surfaces are re-emitted from the new one. */
1815
2026
  function watchThemeResets() {
1816
2027
  if (themeObserver !== null || typeof MutationObserver === "undefined") return () => void 0;
1817
2028
  themeObserver = new MutationObserver(() => {
1818
- if (document.body.getAttribute("data-ds-dark-theme") === "dsh-background-by-model") return;
1819
- if (!rHasColor()) return;
2029
+ invalidateHostPalette();
2030
+ if (document.body.getAttribute(DARK_ATTR) === "dsh-background-by-model") return;
2031
+ if (!rHasColor()) {
2032
+ applyCustomTokens(rOps());
2033
+ return;
2034
+ }
1820
2035
  if (themeRaf !== 0) return;
1821
2036
  themeRaf = requestAnimationFrame(() => {
1822
2037
  themeRaf = 0;
1823
- if (document.body.getAttribute("data-ds-dark-theme") === "dsh-background-by-model") return;
2038
+ if (document.body.getAttribute(DARK_ATTR) === "dsh-background-by-model") return;
1824
2039
  reassertScheme();
1825
2040
  });
1826
2041
  });
1827
2042
  themeObserver.observe(document.body, {
1828
2043
  attributes: true,
1829
- attributeFilter: ["data-ds-dark-theme"]
2044
+ attributeFilter: [DARK_ATTR]
1830
2045
  });
1831
2046
  themeObserver.observe(document.documentElement, {
1832
2047
  attributes: true,
1833
- attributeFilter: ["data-ds-dark-theme"]
2048
+ attributeFilter: [DARK_ATTR]
1834
2049
  });
1835
2050
  return () => {
1836
2051
  themeObserver?.disconnect();
@@ -2072,16 +2287,20 @@ window.__ModuleLoader__.load({
2072
2287
  const url = rWp();
2073
2288
  if (url) applyImageWp(url);
2074
2289
  else if (wpEl !== null) dropWpContainer();
2075
- if (rHasColor()) {
2076
- applyCustomTokens(rOps());
2077
- applySettingsOverrides(rSop());
2078
- applyTrajectoryOverrides(rTrajectoryOpacity());
2079
- } else {
2290
+ if (!rHasColor() && !baseTokenKey.startsWith("host:")) {
2291
+ clearCustomTokens();
2292
+ invalidateHostPalette();
2293
+ }
2294
+ if (!rHasColor()) adoptHostScheme();
2295
+ const src = palette();
2296
+ document.documentElement.toggleAttribute(THEMED_ATTR, src !== null);
2297
+ if (src === null) {
2080
2298
  clearCustomTokens();
2081
- document.body.removeAttribute("data-ds-dark-theme");
2082
- baseTokenKey = "";
2083
- lastBgKey = "";
2299
+ clearOwnedSurfaces();
2084
2300
  }
2301
+ applyCustomTokens(rOps());
2302
+ applySettingsOverrides(rSop());
2303
+ applyTrajectoryOverrides(rTrajectoryOpacity());
2085
2304
  applyRightbarOverrides(rRightbarOpacity());
2086
2305
  applyPartBlurs(rBlurs());
2087
2306
  }
@@ -2091,23 +2310,17 @@ window.__ModuleLoader__.load({
2091
2310
  tokenStyleEl?.remove();
2092
2311
  tokenStyleEl = null;
2093
2312
  removeViewCards();
2094
- document.body.removeAttribute("data-ds-dark-theme");
2313
+ document.documentElement.removeAttribute(THEMED_ATTR);
2095
2314
  document.body.style.removeProperty("color-scheme");
2096
- document.documentElement.style.removeProperty("--dsh-any-bg-settings-surface");
2097
- document.documentElement.style.removeProperty("--dsh-any-bg-settings-layer-1");
2098
- document.documentElement.style.removeProperty("--dsh-any-bg-settings-layer-2");
2099
- document.documentElement.style.removeProperty("--dsh-any-bg-settings-layer-3");
2100
- document.documentElement.style.removeProperty("--dsh-any-traj-layer-1");
2101
- document.documentElement.style.removeProperty("--dsh-any-traj-layer-2");
2102
- document.documentElement.style.removeProperty("--dsh-any-traj-layer-3");
2315
+ clearOwnedSurfaces();
2316
+ adoptHostScheme();
2317
+ invalidateHostPalette();
2103
2318
  document.documentElement.style.removeProperty("--dsh-any-blur-settings");
2104
2319
  document.documentElement.style.removeProperty("--dsh-any-blur-card-panels");
2105
2320
  document.documentElement.style.removeProperty("--dsh-any-input-blur");
2106
2321
  document.documentElement.style.removeProperty("--dsh-any-bg-rightbar");
2107
2322
  document.documentElement.style.removeProperty("--dsh-any-blur-rightbar");
2108
2323
  for (const v of Object.values(OPACITY_VARS)) document.documentElement.style.removeProperty(v);
2109
- baseTokenKey = "";
2110
- lastBgKey = "";
2111
2324
  if (tokensRaf !== null) {
2112
2325
  cancelAnimationFrame(tokensRaf);
2113
2326
  tokensRaf = null;
@@ -2119,9 +2332,6 @@ window.__ModuleLoader__.load({
2119
2332
  setBlur(sidebarEl, 0);
2120
2333
  setBlur(centerEl, 0);
2121
2334
  setBlur(detailsEl, 0);
2122
- if (frameEl !== null) frameEl.style.removeProperty("background");
2123
- if (centerEl !== null) centerEl.style.removeProperty("background");
2124
- if (detailsEl !== null) detailsEl.style.removeProperty("background");
2125
2335
  stopWatchingParts();
2126
2336
  }
2127
2337
  //#endregion
@@ -3919,6 +4129,23 @@ window.__ModuleLoader__.load({
3919
4129
  setExtracting(false);
3920
4130
  }
3921
4131
  };
4132
+ /**
4133
+ * Leaving "follow the system theme" must not hand the user a color that has
4134
+ * nothing to do with the wallpaper they just picked, so this tries the image
4135
+ * first and only falls back to the seed. (`extractColor` is the same pass the
4136
+ * extract button runs and returns false when nothing vivid was found.)
4137
+ */
4138
+ const onPickColor = async () => {
4139
+ if (url !== null) {
4140
+ setExtracting(true);
4141
+ try {
4142
+ if (await p.extractColor(rule.id)) return;
4143
+ } catch {} finally {
4144
+ setExtracting(false);
4145
+ }
4146
+ }
4147
+ p.setRule(rule.id, { color: SEED_COLOR });
4148
+ };
3922
4149
  const cls = `dab-rule${active ? " is-active" : ""}${rule.enabled ? "" : " is-off"}`;
3923
4150
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3924
4151
  className: cls,
@@ -4110,25 +4337,34 @@ window.__ModuleLoader__.load({
4110
4337
  className: "dab-rule-section-title",
4111
4338
  children: t("ruleColor")
4112
4339
  }),
4113
- rule.color === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4114
- className: "dab-chip-row",
4115
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4116
- type: "button",
4117
- className: "dab-btn",
4118
- onClick: () => p.setRule(rule.id, { color: SEED_COLOR }),
4119
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropletIcon, { size: 14 }), t("ruleColorNone")]
4120
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4121
- type: "button",
4122
- className: "dab-btn",
4123
- disabled: url === null || extracting,
4124
- onClick: () => void onExtract(),
4125
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 }), extracting ? t("extracting") : t("ruleColorExtract")]
4126
- })]
4127
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4128
- className: "dab-hint",
4129
- style: { marginTop: 8 },
4130
- children: t("ruleColorNoneHint")
4131
- })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4340
+ rule.color === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4342
+ className: "dab-chip-row",
4343
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4344
+ type: "button",
4345
+ className: "dab-btn",
4346
+ disabled: extracting,
4347
+ onClick: () => void onPickColor(),
4348
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropletIcon, { size: 14 }), extracting ? t("extracting") : t("ruleColorPick")]
4349
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4350
+ type: "button",
4351
+ className: "dab-btn",
4352
+ disabled: url === null || extracting,
4353
+ onClick: () => void onExtract(),
4354
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 }), extracting ? t("extracting") : t("ruleColorExtract")]
4355
+ })]
4356
+ }),
4357
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4358
+ className: "dab-hint",
4359
+ style: { marginTop: 8 },
4360
+ children: t("ruleColorNoneHint")
4361
+ }),
4362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4363
+ className: "dab-hint",
4364
+ style: { marginTop: 6 },
4365
+ children: t("ruleColorPickHint")
4366
+ })
4367
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4132
4368
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4133
4369
  style: {
4134
4370
  display: "flex",
@@ -4522,7 +4758,7 @@ window.__ModuleLoader__.load({
4522
4758
  const styleEl = document.createElement("style");
4523
4759
  styleEl.dataset.plugin = "dsh-background-by-model";
4524
4760
  markOwnSheet(styleEl);
4525
- styleEl.textContent = `body[data-ds-dark-theme="dsh-background-by-model"]::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}${TRAJECTORY_STYLE_RULE}${RIGHTBAR_STYLE_RULE}${INPUT_BLUR_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}`;
4761
+ styleEl.textContent = `body[data-ds-dark-theme="${OWN_SCHEME}"]::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}${TRAJECTORY_STYLE_RULE}${RIGHTBAR_STYLE_RULE}${INPUT_BLUR_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}`;
4526
4762
  document.head.appendChild(styleEl);
4527
4763
  ctx.effect(() => () => {
4528
4764
  styleEl?.parentNode?.removeChild(styleEl);
@@ -4562,20 +4798,38 @@ window.__ModuleLoader__.load({
4562
4798
  rev++;
4563
4799
  bound?.sync(rWp(), rev, rulesRev, modelLabel !== "" ? modelLabel : modelText, modelSource, modelNote, activeRuleId, activeMatched);
4564
4800
  };
4801
+ /**
4802
+ * Publish the scheme the host's own preference resolves to. The theme service
4803
+ * is the only place that knows it (a custom active theme included); the
4804
+ * wallpaper layer needs it to hand `data-ds-dark-theme` back in the host's own
4805
+ * form while a color-less rule follows that theme, instead of leaving the flag
4806
+ * off — which pins the host to its LIGHT palette on a dark system.
4807
+ */
4808
+ const syncHostScheme = () => {
4809
+ try {
4810
+ setHostScheme(ctx.theme.getTheme().active.colorScheme === "dark");
4811
+ } catch {
4812
+ setHostScheme(null);
4813
+ }
4814
+ };
4565
4815
  /** Resolve the active rule for the current model and repaint everything. */
4566
4816
  const applyActive = () => {
4567
4817
  const { rule, matched } = matchRule(cfg.rules, modelText);
4568
4818
  setActive(rule === null ? null : rule.id, matched);
4569
4819
  const color = rule === null ? null : rule.color;
4570
4820
  if (skinTimer !== null) window.clearTimeout(skinTimer);
4571
- skinTimer = window.setTimeout(() => {
4821
+ if (color === null) {
4822
+ skinTimer = null;
4823
+ dropCustom();
4824
+ } else skinTimer = window.setTimeout(() => {
4572
4825
  skinTimer = null;
4573
- if (color === null) dropCustom();
4574
- else registerCustom(color[0], color[1], color[2]);
4826
+ registerCustom(color[0], color[1], color[2]);
4575
4827
  }, 60);
4828
+ syncHostScheme();
4576
4829
  applyWp();
4577
4830
  sync();
4578
4831
  };
4832
+ syncHostScheme();
4579
4833
  applyWp();
4580
4834
  sync();
4581
4835
  watchParts();
@@ -4625,6 +4879,7 @@ window.__ModuleLoader__.load({
4625
4879
  teardownWp();
4626
4880
  }, "dsh-background-by-model: wp cleanup");
4627
4881
  ctx.effect(() => ctx.on("theme/change", () => {
4882
+ syncHostScheme();
4628
4883
  if (activeRuleColor() !== null) {
4629
4884
  const snapshot = ctx.theme.getTheme();
4630
4885
  if (snapshot.preference !== CUSTOM_ID && snapshot.themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
@@ -4927,6 +5182,7 @@ window.__ModuleLoader__.load({
4927
5182
  navIconObserver = null;
4928
5183
  }, "dsh-background-by-model: nav icon watch");
4929
5184
  const restoreSaved = () => {
5185
+ syncHostScheme();
4930
5186
  const color = activeRuleColor();
4931
5187
  if (color !== null) {
4932
5188
  const snapshot = ctx.theme.getTheme();
@@ -4940,8 +5196,12 @@ window.__ModuleLoader__.load({
4940
5196
  restoreTimers.forEach((id) => window.clearTimeout(id));
4941
5197
  }, "dsh-background-by-model: boot restore");
4942
5198
  const watchdogId = window.setInterval(() => {
5199
+ syncHostScheme();
4943
5200
  const color = activeRuleColor();
4944
- if (color === null) return;
5201
+ if (color === null) {
5202
+ refreshSystemTheme();
5203
+ return;
5204
+ }
4945
5205
  const snapshot = ctx.theme.getTheme();
4946
5206
  let changed = false;
4947
5207
  if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) {