dsh-background-by-model 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -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",
@@ -1341,6 +1345,24 @@ window.__ModuleLoader__.load({
1341
1345
  }
1342
1346
  //#endregion
1343
1347
  //#region src/client/wallpaper.ts
1348
+ /**
1349
+ * `data-ds-dark-theme` value the plugin writes while it forces its own dark
1350
+ * palette. The host treats the attribute as a boolean (`toggleAttribute`), so a
1351
+ * plugin-specific value still matches every `body[data-ds-dark-theme]` rule
1352
+ * while staying distinguishable from the host's own write — which is what lets
1353
+ * `releaseOwnScheme` hand the flag back without clobbering the host's scheme.
1354
+ */
1355
+ const OWN_SCHEME = "dsh-background-by-model";
1356
+ const DARK_ATTR = "data-ds-dark-theme";
1357
+ /**
1358
+ * Marker on <html> while the plugin owns a palette. The dialog/trajectory rules
1359
+ * below redirect NO-fallback tokens (`--dsw-alias-bg-layer-*`) to plugin-owned
1360
+ * variables, so they may only match while those variables are actually written:
1361
+ * without the gate a palette-less state would leave them reading a
1362
+ * guaranteed-invalid value (transparent panels) instead of handing the surface
1363
+ * back to the host untouched.
1364
+ */
1365
+ const THEMED_ATTR = "data-dab-themed";
1344
1366
  let wpEl = null;
1345
1367
  let appliedTokenNames = [];
1346
1368
  let tokenStyleEl = null;
@@ -1356,6 +1378,22 @@ window.__ModuleLoader__.load({
1356
1378
  if (tokenStyleEl) tokenStyleEl.textContent = "";
1357
1379
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1358
1380
  appliedTokenNames = [];
1381
+ baseTokenKey = "";
1382
+ lastBgKey = "";
1383
+ }
1384
+ /** Drop every surface the plugin owns: the variables the scoped
1385
+ * dialog/trajectory rules read, and the inline backgrounds written onto the
1386
+ * AppFrame columns. Used when no palette is available and at teardown — without
1387
+ * it the last color's surfaces outlive the color that produced them. */
1388
+ function clearOwnedSurfaces() {
1389
+ discoverParts();
1390
+ const root = document.documentElement;
1391
+ for (const name of OWNED_SURFACE_VARS) root.style.removeProperty(name);
1392
+ for (const el of [
1393
+ frameEl,
1394
+ centerEl,
1395
+ detailsEl
1396
+ ]) el?.style.removeProperty("background");
1359
1397
  }
1360
1398
  const OPACITY_TOKEN_GROUPS = [
1361
1399
  {
@@ -1392,6 +1430,142 @@ window.__ModuleLoader__.load({
1392
1430
  "--dsw-menu-surface-fill": "--dsh-any-op-menu"
1393
1431
  };
1394
1432
  let baseTokenKey = "";
1433
+ const HOST_SURFACE_TOKENS = Object.keys(OPACITY_VARS);
1434
+ /** Plugin-owned variables the scoped dialog/trajectory rules read. */
1435
+ const SETTINGS_SURFACE_VARS = [
1436
+ "--dsh-any-bg-settings-surface",
1437
+ "--dsh-any-bg-settings-layer-1",
1438
+ "--dsh-any-bg-settings-layer-2",
1439
+ "--dsh-any-bg-settings-layer-3"
1440
+ ];
1441
+ const TRAJECTORY_SURFACE_VARS = [
1442
+ "--dsh-any-traj-layer-1",
1443
+ "--dsh-any-traj-layer-2",
1444
+ "--dsh-any-traj-layer-3"
1445
+ ];
1446
+ const OWNED_SURFACE_VARS = [...SETTINGS_SURFACE_VARS, ...TRAJECTORY_SURFACE_VARS];
1447
+ let hostProbe = null;
1448
+ /** Resolve any color the engine understands (hex, rgb()/hsl(), a var() chain,
1449
+ * color-mix(), …) to its USED value: only a used color can be re-emitted with
1450
+ * an alpha. Returns null when the engine rejects the value. */
1451
+ function resolveCssColor(raw) {
1452
+ const value = raw.trim();
1453
+ if (value === "") return null;
1454
+ if (hostProbe === null) {
1455
+ hostProbe = document.createElement("span");
1456
+ hostProbe.setAttribute("aria-hidden", "true");
1457
+ hostProbe.style.cssText = "position:absolute;width:0;height:0;pointer-events:none";
1458
+ }
1459
+ if (!hostProbe.isConnected) document.body.appendChild(hostProbe);
1460
+ hostProbe.style.color = "";
1461
+ hostProbe.style.color = value;
1462
+ if (hostProbe.style.color === "") return null;
1463
+ return getComputedStyle(hostProbe).color || null;
1464
+ }
1465
+ /** Read the host's own surface colors out of the live cascade. */
1466
+ function readHostPalette() {
1467
+ const sheet = tokenStyleEl;
1468
+ const muted = sheet !== null && sheet.isConnected && !sheet.disabled;
1469
+ if (muted) sheet.disabled = true;
1470
+ try {
1471
+ const computed = getComputedStyle(document.body);
1472
+ const tokens = {};
1473
+ for (const name of HOST_SURFACE_TOKENS) {
1474
+ const used = resolveCssColor(computed.getPropertyValue(name));
1475
+ if (used !== null) tokens[name] = used;
1476
+ }
1477
+ return tokens["--dsw-alias-bg-base"] === void 0 ? null : {
1478
+ own: false,
1479
+ tokens
1480
+ };
1481
+ } finally {
1482
+ if (muted) sheet.disabled = false;
1483
+ }
1484
+ }
1485
+ let hostPaletteCache = null;
1486
+ const HOST_PALETTE_TTL_MS = 1e3;
1487
+ function invalidateHostPalette() {
1488
+ hostPaletteCache = null;
1489
+ }
1490
+ function hostPalette() {
1491
+ const now = Date.now();
1492
+ if (hostPaletteCache !== null && now - hostPaletteCache.at < HOST_PALETTE_TTL_MS) return hostPaletteCache.palette;
1493
+ const read = readHostPalette();
1494
+ hostPaletteCache = {
1495
+ at: now,
1496
+ palette: read
1497
+ };
1498
+ return read;
1499
+ }
1500
+ /** The palette every surface is currently painted from. */
1501
+ function palette() {
1502
+ if (!rHasColor()) return hostPalette();
1503
+ const [h, s, l] = rColor();
1504
+ return {
1505
+ own: true,
1506
+ tokens: genTokens(h, s, l).tokens
1507
+ };
1508
+ }
1509
+ /** A resolved color with a new alpha. The plugin's own tokens are `hsl()` and
1510
+ * the host's alias tokens resolve to hex, both of which `toRgba` parses; a host
1511
+ * publishing an already-composited value (`color(…)`, `oklab(…)`, …) cannot be
1512
+ * parsed there, and emitting it unchanged would paint an OPAQUE surface — the
1513
+ * one outcome the transparent-surface design must never fall back to. Those go
1514
+ * through `color-mix` instead. */
1515
+ function withAlpha(color, alpha) {
1516
+ const converted = toRgba(color, alpha);
1517
+ if (/^(rgb|hsl)a?\(/i.test(converted)) return converted;
1518
+ return `color-mix(in srgb, ${color} ${Math.round(Math.max(0, Math.min(1, alpha)) * 100)}%, transparent)`;
1519
+ }
1520
+ /**
1521
+ * The color scheme the HOST's own preference resolves to, pushed in by the
1522
+ * browser half (the theme service knows it, custom themes included). `null`
1523
+ * means "unknown", which is the only case where the plugin falls back to the
1524
+ * preference the host's boot script published and to the OS query itself.
1525
+ */
1526
+ let hostSchemeDark = null;
1527
+ function setHostScheme(dark) {
1528
+ hostSchemeDark = dark;
1529
+ }
1530
+ function resolveHostDark() {
1531
+ if (typeof hostSchemeDark === "boolean") return hostSchemeDark;
1532
+ const source = document.documentElement.dataset.dsThemeSource;
1533
+ if (source === "dark") return true;
1534
+ if (source === "light") return false;
1535
+ try {
1536
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
1537
+ } catch {
1538
+ return false;
1539
+ }
1540
+ }
1541
+ /**
1542
+ * Keep `data-ds-dark-theme` in the HOST's own form (a boolean attribute) while
1543
+ * the plugin does not own a palette.
1544
+ *
1545
+ * The plugin overwrites that flag whenever it owns a palette — including
1546
+ * REMOVING it to force a light one — and the host only re-projects the flag on a
1547
+ * theme change. So a flag the plugin dropped can stick: the host keeps painting
1548
+ * its light palette on a dark system, the readback then hands those light
1549
+ * surfaces back to the plugin, and the interface stays light until some
1550
+ * unrelated event happens to re-run an apply. Restoring the flag from the
1551
+ * resolved scheme closes that hole, and the write is skipped when it already
1552
+ * matches so this never churns the theme observer.
1553
+ */
1554
+ function adoptHostScheme() {
1555
+ const dark = resolveHostDark();
1556
+ const current = document.body.getAttribute(DARK_ATTR);
1557
+ if (dark) {
1558
+ if (current !== "") document.body.setAttribute(DARK_ATTR, "");
1559
+ } else if (current !== null) document.body.removeAttribute(DARK_ATTR);
1560
+ }
1561
+ /** Re-read the host palette and repaint from it when it moved. Called while a
1562
+ * color-less rule is active (the host can re-project its palette at any time,
1563
+ * with no event of ours to hang a repaint on). */
1564
+ function refreshSystemTheme() {
1565
+ adoptHostScheme();
1566
+ invalidateHostPalette();
1567
+ applyCustomTokens(rOps());
1568
+ }
1395
1569
  let pendingOps = null;
1396
1570
  let tokensRaf = null;
1397
1571
  function applyCustomTokens(ops) {
@@ -1407,26 +1581,38 @@ window.__ModuleLoader__.load({
1407
1581
  }
1408
1582
  let lastBgKey = "";
1409
1583
  function applyCustomTokensNow(ops) {
1584
+ const src = palette();
1585
+ if (src === null) {
1586
+ clearCustomTokens();
1587
+ clearOwnedSurfaces();
1588
+ adoptHostScheme();
1589
+ return;
1590
+ }
1591
+ const { own, tokens } = src;
1410
1592
  const [h, s, l] = rColor();
1411
- const { tokens } = genTokens(h, s, l);
1412
1593
  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"}`];
1594
+ const forceDark = own && l < .55;
1595
+ const key = own ? `own:${h}|${s}|${l}` : `host:${HOST_SURFACE_TOKENS.map((n) => tokens[n] ?? "").join("|")}`;
1596
+ if (key !== baseTokenKey) {
1597
+ if (own) {
1598
+ if (forceDark) document.body.setAttribute(DARK_ATTR, OWN_SCHEME);
1599
+ else document.body.removeAttribute(DARK_ATTR);
1600
+ } else adoptHostScheme();
1601
+ const decls = own ? [`color-scheme:${forceDark ? "dark" : "light"}`] : [];
1419
1602
  for (const [name, value] of Object.entries(tokens)) {
1420
1603
  const opVar = OPACITY_VARS[name];
1421
1604
  decls.push(`${name}:${opVar !== void 0 ? `var(${opVar})` : value}!important`);
1422
1605
  }
1423
1606
  ensureTokenStyle().textContent = `body{${decls.join(";")}}`;
1424
- for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1425
- appliedTokenNames = Object.keys(tokens);
1607
+ if (own) {
1608
+ for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1609
+ appliedTokenNames = Object.keys(tokens);
1610
+ }
1611
+ baseTokenKey = key;
1426
1612
  }
1427
1613
  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));
1614
+ 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]));
1615
+ root.style.setProperty("--dsh-any-op-menu-cordis", withAlpha(tokens["--dsw-specific-menu"] ?? "#000", ops.input));
1430
1616
  const bgKey = `${baseTokenKey}|${ops.bg}`;
1431
1617
  if (bgKey !== lastBgKey) {
1432
1618
  lastBgKey = bgKey;
@@ -1435,45 +1621,48 @@ window.__ModuleLoader__.load({
1435
1621
  } catch {}
1436
1622
  }
1437
1623
  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)}`;
1624
+ 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
1625
  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
1626
  function applyInputBlur(px) {
1441
1627
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-input-blur", `blur(${px}px)`);
1442
1628
  else document.documentElement.style.removeProperty("--dsh-any-input-blur");
1443
1629
  }
1444
1630
  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)}";
1631
+ const tokens = palette()?.tokens ?? {};
1632
+ const root = document.documentElement;
1633
+ const write = (name, token) => {
1634
+ const value = tokens[token];
1635
+ if (value === void 0) root.style.removeProperty(name);
1636
+ else root.style.setProperty(name, withAlpha(value, op));
1637
+ };
1638
+ write("--dsh-any-bg-settings-surface", "--dsw-alias-bg-layer-2");
1639
+ write("--dsh-any-bg-settings-layer-1", "--dsw-alias-bg-layer-1");
1640
+ write("--dsh-any-bg-settings-layer-2", "--dsw-alias-bg-layer-2");
1641
+ write("--dsh-any-bg-settings-layer-3", "--dsw-alias-bg-layer-3");
1642
+ }
1643
+ 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
1644
  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));
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-traj-layer-1", "--dsw-alias-bg-layer-1");
1653
+ write("--dsh-any-traj-layer-2", "--dsw-alias-bg-layer-2");
1654
+ write("--dsh-any-traj-layer-3", "--dsw-alias-bg-layer-3");
1465
1655
  }
1466
1656
  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
1657
  /** Paint the file-preview panel's surface from its own slider. */
1468
1658
  function applyRightbarOverrides(op) {
1469
1659
  const root = document.documentElement;
1470
- if (!rHasColor()) {
1660
+ const base = palette()?.tokens["--dsw-alias-bg-base"];
1661
+ if (base === void 0) {
1471
1662
  root.style.removeProperty("--dsh-any-bg-rightbar");
1472
1663
  return;
1473
1664
  }
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));
1665
+ root.style.setProperty("--dsh-any-bg-rightbar", withAlpha(base, op));
1477
1666
  }
1478
1667
  /** Extra backdrop frost for the file-preview panel (0 = no frost of its own). */
1479
1668
  function applyRightbarBlur(px) {
@@ -1542,14 +1731,20 @@ window.__ModuleLoader__.load({
1542
1731
  * reducing the main-bg opacity stacked a second alpha onto the sidebar; moving
1543
1732
  * the alpha onto the columns keeps the sidebar owned by its own slider. */
1544
1733
  function applyPartOpacities(ops) {
1545
- if (!rHasColor()) return;
1546
1734
  discoverParts();
1547
1735
  if (frameEl === null) return;
1548
- const [h, s, l] = rColor();
1549
- const base = genTokens(h, s, l).tokens["--dsw-alias-bg-base"];
1736
+ const base = palette()?.tokens["--dsw-alias-bg-base"];
1737
+ if (base === void 0) {
1738
+ for (const el of [
1739
+ frameEl,
1740
+ centerEl,
1741
+ detailsEl
1742
+ ]) el?.style.removeProperty("background");
1743
+ return;
1744
+ }
1550
1745
  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";
1746
+ if (centerEl !== null) centerEl.style.background = withAlpha(base, ops.bg);
1747
+ if (detailsEl !== null) detailsEl.style.background = withAlpha(base, ops.bg);
1553
1748
  }
1554
1749
  /** Blur of the option panels inside the settings dialog (.dab-card), owned by
1555
1750
  * the "dialog option panel" (card) blur slider. Written as a plugin-owned
@@ -1762,8 +1957,7 @@ window.__ModuleLoader__.load({
1762
1957
  function applyViewCards() {
1763
1958
  discoverParts();
1764
1959
  if (centerEl === null) return;
1765
- const [h, s, l] = rColor();
1766
- const surface = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-1"];
1960
+ const surface = palette()?.tokens["--dsw-alias-bg-layer-1"];
1767
1961
  VIEW_CARDS.forEach((spec, i) => {
1768
1962
  const target = discoverViewTarget(i, spec);
1769
1963
  if (target === null) return;
@@ -1773,8 +1967,8 @@ window.__ModuleLoader__.load({
1773
1967
  if (!plain) {
1774
1968
  if (!target.hasAttribute(spec.mark)) stashCardPrev(target, spec.prev, false);
1775
1969
  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";
1970
+ target.style.background = surface !== void 0 ? withAlpha(surface, opacity) : "transparent";
1971
+ target.style.border = surface !== void 0 ? `1px solid ${withAlpha(surface, borderAlpha)}` : "1px solid transparent";
1778
1972
  target.style.borderRadius = "16px";
1779
1973
  target.style.padding = "18px";
1780
1974
  }
@@ -1806,31 +2000,38 @@ window.__ModuleLoader__.load({
1806
2000
  let themeRaf = 0;
1807
2001
  function reassertScheme() {
1808
2002
  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");
2003
+ if (l < .55) document.body.setAttribute(DARK_ATTR, OWN_SCHEME);
2004
+ else document.body.removeAttribute(DARK_ATTR);
1811
2005
  applyCustomTokens(rOps());
1812
2006
  }
1813
2007
  /** 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. */
2008
+ * refresh / cold-load / set-change never flashes a light frame. Also the point
2009
+ * where a host-side scheme flip is noticed while a color-less rule runs: that
2010
+ * rule paints from the host palette, so the readback is dropped and the
2011
+ * surfaces are re-emitted from the new one. */
1815
2012
  function watchThemeResets() {
1816
2013
  if (themeObserver !== null || typeof MutationObserver === "undefined") return () => void 0;
1817
2014
  themeObserver = new MutationObserver(() => {
1818
- if (document.body.getAttribute("data-ds-dark-theme") === "dsh-background-by-model") return;
1819
- if (!rHasColor()) return;
2015
+ invalidateHostPalette();
2016
+ if (document.body.getAttribute(DARK_ATTR) === "dsh-background-by-model") return;
2017
+ if (!rHasColor()) {
2018
+ applyCustomTokens(rOps());
2019
+ return;
2020
+ }
1820
2021
  if (themeRaf !== 0) return;
1821
2022
  themeRaf = requestAnimationFrame(() => {
1822
2023
  themeRaf = 0;
1823
- if (document.body.getAttribute("data-ds-dark-theme") === "dsh-background-by-model") return;
2024
+ if (document.body.getAttribute(DARK_ATTR) === "dsh-background-by-model") return;
1824
2025
  reassertScheme();
1825
2026
  });
1826
2027
  });
1827
2028
  themeObserver.observe(document.body, {
1828
2029
  attributes: true,
1829
- attributeFilter: ["data-ds-dark-theme"]
2030
+ attributeFilter: [DARK_ATTR]
1830
2031
  });
1831
2032
  themeObserver.observe(document.documentElement, {
1832
2033
  attributes: true,
1833
- attributeFilter: ["data-ds-dark-theme"]
2034
+ attributeFilter: [DARK_ATTR]
1834
2035
  });
1835
2036
  return () => {
1836
2037
  themeObserver?.disconnect();
@@ -2072,16 +2273,20 @@ window.__ModuleLoader__.load({
2072
2273
  const url = rWp();
2073
2274
  if (url) applyImageWp(url);
2074
2275
  else if (wpEl !== null) dropWpContainer();
2075
- if (rHasColor()) {
2076
- applyCustomTokens(rOps());
2077
- applySettingsOverrides(rSop());
2078
- applyTrajectoryOverrides(rTrajectoryOpacity());
2079
- } else {
2276
+ if (!rHasColor() && !baseTokenKey.startsWith("host:")) {
2277
+ clearCustomTokens();
2278
+ invalidateHostPalette();
2279
+ }
2280
+ if (!rHasColor()) adoptHostScheme();
2281
+ const src = palette();
2282
+ document.documentElement.toggleAttribute(THEMED_ATTR, src !== null);
2283
+ if (src === null) {
2080
2284
  clearCustomTokens();
2081
- document.body.removeAttribute("data-ds-dark-theme");
2082
- baseTokenKey = "";
2083
- lastBgKey = "";
2285
+ clearOwnedSurfaces();
2084
2286
  }
2287
+ applyCustomTokens(rOps());
2288
+ applySettingsOverrides(rSop());
2289
+ applyTrajectoryOverrides(rTrajectoryOpacity());
2085
2290
  applyRightbarOverrides(rRightbarOpacity());
2086
2291
  applyPartBlurs(rBlurs());
2087
2292
  }
@@ -2091,23 +2296,17 @@ window.__ModuleLoader__.load({
2091
2296
  tokenStyleEl?.remove();
2092
2297
  tokenStyleEl = null;
2093
2298
  removeViewCards();
2094
- document.body.removeAttribute("data-ds-dark-theme");
2299
+ document.documentElement.removeAttribute(THEMED_ATTR);
2095
2300
  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");
2301
+ clearOwnedSurfaces();
2302
+ adoptHostScheme();
2303
+ invalidateHostPalette();
2103
2304
  document.documentElement.style.removeProperty("--dsh-any-blur-settings");
2104
2305
  document.documentElement.style.removeProperty("--dsh-any-blur-card-panels");
2105
2306
  document.documentElement.style.removeProperty("--dsh-any-input-blur");
2106
2307
  document.documentElement.style.removeProperty("--dsh-any-bg-rightbar");
2107
2308
  document.documentElement.style.removeProperty("--dsh-any-blur-rightbar");
2108
2309
  for (const v of Object.values(OPACITY_VARS)) document.documentElement.style.removeProperty(v);
2109
- baseTokenKey = "";
2110
- lastBgKey = "";
2111
2310
  if (tokensRaf !== null) {
2112
2311
  cancelAnimationFrame(tokensRaf);
2113
2312
  tokensRaf = null;
@@ -2119,9 +2318,6 @@ window.__ModuleLoader__.load({
2119
2318
  setBlur(sidebarEl, 0);
2120
2319
  setBlur(centerEl, 0);
2121
2320
  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
2321
  stopWatchingParts();
2126
2322
  }
2127
2323
  //#endregion
@@ -3919,6 +4115,23 @@ window.__ModuleLoader__.load({
3919
4115
  setExtracting(false);
3920
4116
  }
3921
4117
  };
4118
+ /**
4119
+ * Leaving "follow the system theme" must not hand the user a color that has
4120
+ * nothing to do with the wallpaper they just picked, so this tries the image
4121
+ * first and only falls back to the seed. (`extractColor` is the same pass the
4122
+ * extract button runs and returns false when nothing vivid was found.)
4123
+ */
4124
+ const onPickColor = async () => {
4125
+ if (url !== null) {
4126
+ setExtracting(true);
4127
+ try {
4128
+ if (await p.extractColor(rule.id)) return;
4129
+ } catch {} finally {
4130
+ setExtracting(false);
4131
+ }
4132
+ }
4133
+ p.setRule(rule.id, { color: SEED_COLOR });
4134
+ };
3922
4135
  const cls = `dab-rule${active ? " is-active" : ""}${rule.enabled ? "" : " is-off"}`;
3923
4136
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3924
4137
  className: cls,
@@ -4110,25 +4323,34 @@ window.__ModuleLoader__.load({
4110
4323
  className: "dab-rule-section-title",
4111
4324
  children: t("ruleColor")
4112
4325
  }),
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: [
4326
+ rule.color === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4328
+ className: "dab-chip-row",
4329
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4330
+ type: "button",
4331
+ className: "dab-btn",
4332
+ disabled: extracting,
4333
+ onClick: () => void onPickColor(),
4334
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropletIcon, { size: 14 }), extracting ? t("extracting") : t("ruleColorPick")]
4335
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4336
+ type: "button",
4337
+ className: "dab-btn",
4338
+ disabled: url === null || extracting,
4339
+ onClick: () => void onExtract(),
4340
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 14 }), extracting ? t("extracting") : t("ruleColorExtract")]
4341
+ })]
4342
+ }),
4343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4344
+ className: "dab-hint",
4345
+ style: { marginTop: 8 },
4346
+ children: t("ruleColorNoneHint")
4347
+ }),
4348
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4349
+ className: "dab-hint",
4350
+ style: { marginTop: 6 },
4351
+ children: t("ruleColorPickHint")
4352
+ })
4353
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4132
4354
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4133
4355
  style: {
4134
4356
  display: "flex",
@@ -4522,7 +4744,7 @@ window.__ModuleLoader__.load({
4522
4744
  const styleEl = document.createElement("style");
4523
4745
  styleEl.dataset.plugin = "dsh-background-by-model";
4524
4746
  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}`;
4747
+ 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
4748
  document.head.appendChild(styleEl);
4527
4749
  ctx.effect(() => () => {
4528
4750
  styleEl?.parentNode?.removeChild(styleEl);
@@ -4562,20 +4784,38 @@ window.__ModuleLoader__.load({
4562
4784
  rev++;
4563
4785
  bound?.sync(rWp(), rev, rulesRev, modelLabel !== "" ? modelLabel : modelText, modelSource, modelNote, activeRuleId, activeMatched);
4564
4786
  };
4787
+ /**
4788
+ * Publish the scheme the host's own preference resolves to. The theme service
4789
+ * is the only place that knows it (a custom active theme included); the
4790
+ * wallpaper layer needs it to hand `data-ds-dark-theme` back in the host's own
4791
+ * form while a color-less rule follows that theme, instead of leaving the flag
4792
+ * off — which pins the host to its LIGHT palette on a dark system.
4793
+ */
4794
+ const syncHostScheme = () => {
4795
+ try {
4796
+ setHostScheme(ctx.theme.getTheme().active.colorScheme === "dark");
4797
+ } catch {
4798
+ setHostScheme(null);
4799
+ }
4800
+ };
4565
4801
  /** Resolve the active rule for the current model and repaint everything. */
4566
4802
  const applyActive = () => {
4567
4803
  const { rule, matched } = matchRule(cfg.rules, modelText);
4568
4804
  setActive(rule === null ? null : rule.id, matched);
4569
4805
  const color = rule === null ? null : rule.color;
4570
4806
  if (skinTimer !== null) window.clearTimeout(skinTimer);
4571
- skinTimer = window.setTimeout(() => {
4807
+ if (color === null) {
4808
+ skinTimer = null;
4809
+ dropCustom();
4810
+ } else skinTimer = window.setTimeout(() => {
4572
4811
  skinTimer = null;
4573
- if (color === null) dropCustom();
4574
- else registerCustom(color[0], color[1], color[2]);
4812
+ registerCustom(color[0], color[1], color[2]);
4575
4813
  }, 60);
4814
+ syncHostScheme();
4576
4815
  applyWp();
4577
4816
  sync();
4578
4817
  };
4818
+ syncHostScheme();
4579
4819
  applyWp();
4580
4820
  sync();
4581
4821
  watchParts();
@@ -4625,6 +4865,7 @@ window.__ModuleLoader__.load({
4625
4865
  teardownWp();
4626
4866
  }, "dsh-background-by-model: wp cleanup");
4627
4867
  ctx.effect(() => ctx.on("theme/change", () => {
4868
+ syncHostScheme();
4628
4869
  if (activeRuleColor() !== null) {
4629
4870
  const snapshot = ctx.theme.getTheme();
4630
4871
  if (snapshot.preference !== CUSTOM_ID && snapshot.themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
@@ -4927,6 +5168,7 @@ window.__ModuleLoader__.load({
4927
5168
  navIconObserver = null;
4928
5169
  }, "dsh-background-by-model: nav icon watch");
4929
5170
  const restoreSaved = () => {
5171
+ syncHostScheme();
4930
5172
  const color = activeRuleColor();
4931
5173
  if (color !== null) {
4932
5174
  const snapshot = ctx.theme.getTheme();
@@ -4940,8 +5182,12 @@ window.__ModuleLoader__.load({
4940
5182
  restoreTimers.forEach((id) => window.clearTimeout(id));
4941
5183
  }, "dsh-background-by-model: boot restore");
4942
5184
  const watchdogId = window.setInterval(() => {
5185
+ syncHostScheme();
4943
5186
  const color = activeRuleColor();
4944
- if (color === null) return;
5187
+ if (color === null) {
5188
+ refreshSystemTheme();
5189
+ return;
5190
+ }
4945
5191
  const snapshot = ctx.theme.getTheme();
4946
5192
  let changed = false;
4947
5193
  if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) {