dsh-any-background 0.3.8 → 0.3.9

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
@@ -57,6 +57,12 @@ window.__ModuleLoader__.load({
57
57
  uiProduced: "产出物/高亮内容",
58
58
  uiHeader: "顶栏选项",
59
59
  uiHeaderBar: "标题栏",
60
+ captionInkLabel: "标题条带文字",
61
+ captionInkHint: "桌面端顶部那条标题带上的文字(原生「应用 / 编辑」菜单与侧栏开合按钮)由谁上色。条带是侧栏材质压在壁纸上的结果,两者可能一个深一个浅。",
62
+ captionInkAuto: "自动",
63
+ captionInkTheme: "跟随主题",
64
+ captionInkWhite: "始终白色",
65
+ captionInkBlack: "始终黑色",
60
66
  fontPageTitle: "字体与描边",
61
67
  fontTitle: "自定义字体",
62
68
  fontHint: "支持 ttf/otf/woff/woff2 字体文件(上限 100MB),将应用到整个界面的文字;代码块的等宽字体保持不变。中文字体通常有几十 MB,首次上传与刷新加载需要片刻",
@@ -249,6 +255,12 @@ window.__ModuleLoader__.load({
249
255
  uiProduced: "Produced / highlights",
250
256
  uiHeader: "Header popovers",
251
257
  uiHeaderBar: "Header bar",
258
+ captionInkLabel: "Caption strip text",
259
+ captionInkHint: "Who paints the text on the desktop caption strip — the native 「应用 / 编辑」 menu bar and the sidebar toggle. The strip is the sidebar material over the wallpaper, so it can end up on the opposite side of the theme.",
260
+ captionInkAuto: "Auto",
261
+ captionInkTheme: "Follow theme",
262
+ captionInkWhite: "Always white",
263
+ captionInkBlack: "Always black",
252
264
  fontPageTitle: "Font & outline",
253
265
  fontTitle: "Custom font",
254
266
  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",
@@ -518,6 +530,7 @@ window.__ModuleLoader__.load({
518
530
  producedOpacity: .5,
519
531
  headerOpacity: .5,
520
532
  headerBarOpacity: .5,
533
+ captionInk: "auto",
521
534
  profiles: [],
522
535
  rotation: {
523
536
  enabled: false,
@@ -539,6 +552,17 @@ window.__ModuleLoader__.load({
539
552
  activeProfile: null
540
553
  };
541
554
  const clamp01 = (n, def) => typeof n === "number" ? Math.min(1, Math.max(0, n)) : def;
555
+ /** One place decides what a stored / adopted 「标题栏文字」 value means: anything the
556
+ * host half or an older config file did not name is `auto`, which is 0.3.8's behaviour. */
557
+ const CAPTION_INK_MODES = [
558
+ "auto",
559
+ "theme",
560
+ "white",
561
+ "black"
562
+ ];
563
+ function adoptCaptionInk(v) {
564
+ return CAPTION_INK_MODES.includes(v) ? v : DEFAULT_CONFIG.captionInk;
565
+ }
542
566
  let cfg = {
543
567
  ...DEFAULT_CONFIG,
544
568
  opacities: { ...DEFAULT_CONFIG.opacities },
@@ -732,6 +756,10 @@ window.__ModuleLoader__.load({
732
756
  function rHeaderBarOpacity() {
733
757
  return clamp01(cfg.headerBarOpacity, DEFAULT_CONFIG.headerBarOpacity);
734
758
  }
759
+ /** Who owns the desktop caption strip's chrome ink — see `CaptionInkMode`. */
760
+ function rCaptionInk() {
761
+ return adoptCaptionInk(cfg.captionInk);
762
+ }
735
763
  function rBgState() {
736
764
  return cfg.bgState;
737
765
  }
@@ -968,6 +996,7 @@ window.__ModuleLoader__.load({
968
996
  producedOpacity: clamp01(c.producedOpacity, DEFAULT_CONFIG.producedOpacity),
969
997
  headerOpacity: clamp01(c.headerOpacity, DEFAULT_CONFIG.headerOpacity),
970
998
  headerBarOpacity: clamp01(c.headerBarOpacity, DEFAULT_CONFIG.headerBarOpacity),
999
+ captionInk: adoptCaptionInk(c.captionInk),
971
1000
  profiles: adoptProfiles(c.profiles),
972
1001
  rotation: adoptRotation(c.rotation),
973
1002
  schedule: adoptSchedule(c.schedule),
@@ -1304,8 +1333,9 @@ window.__ModuleLoader__.load({
1304
1333
  * painting element. A 15th file names the same token without consuming it:
1305
1334
  * `ui-theme/src/styles/gradient-shadow-text.css` DECLARES it on `body` —
1306
1335
  * `blur(40px) saturate(150%)`, no media query — which is exactly why writing the
1307
- * token on a menu with a `none` fallback deletes a frost the host always had
1308
- * (see `CARD_FROST_GATE` in `wallpaper.ts`). `MenuSurface.tsx` / `.module.css` are
1336
+ * token on a menu with a `none` fallback deletes a frost the host always had.
1337
+ * That is now the intended reading of a 0 on the menu blur sliders — see the note
1338
+ * above `popoverBlurRule` in `wallpaper.ts`. `MenuSurface.tsx` / `.module.css` are
1309
1339
  * byte-identical from rc.2 through `0.2.1-alpha.2`, and the consumer set moved only
1310
1340
  * in line numbers (the same 15 files at every tag in the table, checked per tag).
1311
1341
  *
@@ -2161,16 +2191,18 @@ window.__ModuleLoader__.load({
2161
2191
  * A header dropdown can only be open while its trigger reports
2162
2192
  * `aria-expanded="true"`, and opening one is what mounts the popover. So a
2163
2193
  * popover tag is maintained as: "there is at least one expanded header trigger,
2164
- * AND this popover is not owned by another group". Popovers are only ever
2194
+ * AND this popover is a header candidate" — see `isHeaderSurface` for the two
2195
+ * shapes that qualify (portaled to `<body>`, or mounted inside one of the
2196
+ * header's own slot anchors) and for what stays foreign. Popovers are only ever
2165
2197
  * tagged/released on a change of that combined state, which makes the tag
2166
2198
  * self-consistent and idempotent.
2167
2199
  *
2168
2200
  * The one hazard is a popover that is open for an unrelated reason (a composer
2169
- * menu, a hover card) *while* a header trigger stays expanded. Such a popover
2170
- * is not a candidate: it is excluded by `ownsForeignSurface`, which skips any
2171
- * popover carrying markers of a different slider group. In practice header
2172
- * dropdowns are transient (opening one closes the others), so this is a guard
2173
- * rather than the common path.
2201
+ * menu, a hover card) *while* a header trigger stays expanded. Such a popover is
2202
+ * not a candidate: it is neither portaled nor a descendant of a header anchor,
2203
+ * and the composer's in-place lists keep belonging to the card group. In practice
2204
+ * header dropdowns are transient (opening one closes the others), so this is a
2205
+ * guard rather than the common path.
2174
2206
  *
2175
2207
  * THE SURFACE MARKER IS PART OF THE CANDIDATE SET (`0.2.1-alpha.3` and up).
2176
2208
  * That release moved the background-job list onto the shared `MenuSurface`, whose
@@ -2209,20 +2241,54 @@ window.__ModuleLoader__.load({
2209
2241
  for (const anchor of document.querySelectorAll(headerSlotAnchors())) if (anchor.querySelector("[aria-expanded=\"true\"]") !== null) return true;
2210
2242
  return false;
2211
2243
  }
2212
- /** Surfaces that belong to a different slider group and must never be claimed.
2213
- * `_denseList` without `_portal` is the in-place card-side dense menu, an
2214
- * `_ioCard`/`_menu`-classed body-level list belongs elsewhere, and a modal is
2215
- * its own surface. */
2216
- function ownsForeignSurface(el) {
2217
- if (el.getAttribute("aria-modal") === "true") return true;
2218
- return el.parentElement !== document.body;
2244
+ /** Whether this popover can belong to the header.
2245
+ *
2246
+ * Two shapes, and the second one is the reason this is not just "portaled to body":
2247
+ * `session-log-export/HeaderAction.tsx` renders its `Menu` WITHOUT the `portal` prop,
2248
+ * so the card is mounted in place, inside `[data-slot="conversation.session.header
2249
+ * .utilities"]` — a DOM descendant of the row, exactly where a header trigger's own
2250
+ * popover belongs. Keyed on `parentElement === body` alone it was never claimable, and
2251
+ * the only thing that styled it at all was the header group's
2252
+ * `[role="menu"][class*="_denseList"]:not([class*="_portal"])` arm — which dies the
2253
+ * moment the host drops `dense` (or adds a header popover that is not a dense menu),
2254
+ * and leaves the surface following the card slider while it sits in the title row.
2255
+ *
2256
+ * A popover that is neither portaled nor inside a header anchor stays foreign: that is
2257
+ * the composer's in-place lists (permission / command / model), which the card group
2258
+ * owns and this group must never reach. */
2259
+ function isHeaderSurface(el) {
2260
+ if (el.getAttribute("aria-modal") === "true") return false;
2261
+ return el.parentElement === document.body || el.closest(headerSlotAnchors()) !== null;
2219
2262
  }
2220
2263
  let observer = null;
2221
- let raf = null;
2264
+ let pending = null;
2265
+ /** False once the effect is torn down, so a pass already queued on the microtask queue
2266
+ * cannot re-mark a popover after the teardown cleared it. */
2267
+ let active = false;
2268
+ /** Coalesce the observer's batches into ONE pass, but flush it on the microtask queue
2269
+ * rather than a animation frame.
2270
+ *
2271
+ * The frame version was the bug, not the batching: `requestAnimationFrame` callbacks
2272
+ * do not run while a window is not rendering — a hidden tab, an occluded or minimized
2273
+ * desktop window, a background Electron surface — and a queued frame never fires, so
2274
+ * the tag never lands and the 「顶栏选项」 pair silently keeps whatever the card group
2275
+ * gave the panel. Observed live: with the page not painting, an open header menu stayed
2276
+ * `data-dsh-any-header-popover`-less indefinitely, and the same menu was tagged the
2277
+ * moment rAF was made to run. A `MutationObserver` callback is already delivered once
2278
+ * per batch at microtask time, so a microtask flush coalesces exactly as well and has no
2279
+ * dependency on the compositor at all — and it lands in the SAME frame the popover
2280
+ * mounts, which also removes the one-frame flash of card styling on open. */
2281
+ function schedule() {
2282
+ if (pending !== null || !active) return;
2283
+ pending = Promise.resolve().then(() => {
2284
+ pending = null;
2285
+ if (active) refreshHeaderPopovers();
2286
+ });
2287
+ }
2222
2288
  /** Recompute tags from the current open state. */
2223
2289
  function refreshHeaderPopovers() {
2224
2290
  const open = headerTriggerOpen();
2225
- const candidates = [...document.querySelectorAll(POPOVER_SELECTOR)].filter((el) => !ownsForeignSurface(el));
2291
+ const candidates = [...document.querySelectorAll(POPOVER_SELECTOR)].filter(isHeaderSurface);
2226
2292
  if (!open) {
2227
2293
  for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
2228
2294
  return;
@@ -2230,16 +2296,10 @@ window.__ModuleLoader__.load({
2230
2296
  for (const el of candidates) el.setAttribute(HEADER_POPOVER_ATTR, "");
2231
2297
  for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) if (!candidates.includes(el)) el.removeAttribute(HEADER_POPOVER_ATTR);
2232
2298
  }
2233
- function schedule() {
2234
- if (raf !== null) return;
2235
- raf = window.requestAnimationFrame(() => {
2236
- raf = null;
2237
- refreshHeaderPopovers();
2238
- });
2239
- }
2240
2299
  /** Start tagging. Returns a teardown that stops observing and clears markings. */
2241
2300
  function startHeaderPopoverTagging() {
2242
2301
  if (typeof MutationObserver === "undefined" || typeof document === "undefined") return () => {};
2302
+ active = true;
2243
2303
  refreshHeaderPopovers();
2244
2304
  observer = new MutationObserver(schedule);
2245
2305
  observer.observe(document.body, {
@@ -2249,12 +2309,9 @@ window.__ModuleLoader__.load({
2249
2309
  attributeFilter: ["aria-expanded"]
2250
2310
  });
2251
2311
  return () => {
2312
+ active = false;
2252
2313
  observer?.disconnect();
2253
2314
  observer = null;
2254
- if (raf !== null) {
2255
- window.cancelAnimationFrame(raf);
2256
- raf = null;
2257
- }
2258
2315
  for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
2259
2316
  };
2260
2317
  }
@@ -3610,16 +3667,40 @@ window.__ModuleLoader__.load({
3610
3667
  if (rSchemeOverride() !== "auto") return genTokens(220, .04, scheme === "dark" ? .14 : .92, scheme).tokens;
3611
3668
  return null;
3612
3669
  }
3613
- /** Surface colors the opacity sliders fade when the plugin has NO palette of
3614
- * its own (no picked color, no forced scheme), read live from the host's
3615
- * resolved tokens on `:root` — the same trick applyPanelOverrides uses for the
3616
- * workbench panel. Without this source the per-part alpha is never emitted at
3617
- * all and every opacity slider looks inert until the user drags it once.
3618
- * The sliders only ever supply the alpha: the host still decides the colors,
3619
- * so a custom host skin survives. Returns null when the host exposes none. */
3670
+ /** Surface colors the opacity sliders fade when the plugin has NO palette of its own
3671
+ * (no picked color, no forced scheme), read live from the host's own resolved tokens —
3672
+ * the same trick applyPanelOverrides uses for the workbench panel. The sliders only
3673
+ * ever supply the alpha: the host still decides the colors, so a custom host skin
3674
+ * survives. Returns null when the host exposes none.
3675
+ *
3676
+ * SAMPLED ON `<body>`, NOT `:root`: `design-platform.css` declares this whole token set
3677
+ * inside a `body { … }` block (light at :166, dark at :284), and a custom property is
3678
+ * inherited DOWNWARD only — read off the root element every one of them is the empty
3679
+ * string, this function answers null, `applyCustomTokensNow` takes its `surfaces ===
3680
+ * null` exit, and on an install with no picked color every interface-opacity slider does
3681
+ * nothing at all (the frame never gets cleared either, so the wallpaper stays hidden).
3682
+ * Measured against the host's own declarations: `:root` → all five tokens empty,
3683
+ * `body` → all five resolve.
3684
+ *
3685
+ * …AND CACHED PER HOST GENERATION, with our own sheet switched off while reading: the
3686
+ * plugin re-scopes these same names on `body`
3687
+ * (`body{--dsw-specific-menu:var(--dsh-any-op-menu)!important}`), so a second read after
3688
+ * the first write returns the plugin's OWN substituted literal — the surfaces would
3689
+ * freeze at whatever color they first sampled instead of following the host. The
3690
+ * generation key is what can flip the host's palette without anything in this plugin
3691
+ * moving: its dark-mode marker, and the platform attribute (`[data-platform='darwin']`
3692
+ * re-declares `--dsw-specific-menu`). */
3693
+ let hostSurfaceSample = null;
3620
3694
  function readHostOpacityTokens() {
3621
- if (typeof getComputedStyle === "undefined") return null;
3622
- const cs = getComputedStyle(document.documentElement);
3695
+ if (typeof getComputedStyle === "undefined" || typeof document === "undefined") return null;
3696
+ const body = document.body;
3697
+ if (body === null) return null;
3698
+ const key = `${body.getAttribute("data-ds-dark-theme") ?? ""}|${document.documentElement.getAttribute("data-platform") ?? ""}`;
3699
+ if (hostSurfaceSample !== null && hostSurfaceSample.key === key) return hostSurfaceSample.map;
3700
+ const sheet = tokenStyleEl;
3701
+ const wasDisabled = sheet === null ? false : sheet.disabled;
3702
+ if (sheet !== null) sheet.disabled = true;
3703
+ const cs = getComputedStyle(body);
3623
3704
  const out = {};
3624
3705
  let found = false;
3625
3706
  for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) {
@@ -3628,7 +3709,12 @@ window.__ModuleLoader__.load({
3628
3709
  out[name] = v;
3629
3710
  found = true;
3630
3711
  }
3631
- return found ? out : null;
3712
+ if (sheet !== null) sheet.disabled = wasDisabled;
3713
+ hostSurfaceSample = {
3714
+ key,
3715
+ map: found ? out : null
3716
+ };
3717
+ return hostSurfaceSample.map;
3632
3718
  }
3633
3719
  function applyCustomTokensNow(ops) {
3634
3720
  const hasColor = rHasColor();
@@ -3708,34 +3794,30 @@ window.__ModuleLoader__.load({
3708
3794
  function menuFrost(blurVar) {
3709
3795
  return hostAdapter().menus.frostDecl(`var(${blurVar},none)`);
3710
3796
  }
3711
- /** The `<html>` classes that let the two menu groups write their frost token at all.
3797
+ /** The frost token, asserted ALWAYS — and what that means for the slider's bottom end.
3712
3798
  *
3713
- * WHY A GATE IS NEEDED AND `:where()` IS ENOUGH: the host declares its own menu frost
3714
- * on `body` (`ui-theme/src/styles/gradient-shadow-text.css` —
3715
- * `--dsw-menu-backdrop-filter: blur(40px) saturate(150%)`, no media query), and every
3716
- * menu frosts from that INHERITED value, whether it filters itself (the background-job
3717
- * `<ul>`) or hands the value to a `MenuSurface` `.material` child. An element's own
3718
- * declaration always beats what it inherits, so writing the token with a `none`
3719
- * fallback does not "add no blur" at slider 0 — it DELETES the host's frost. The
3720
- * result is a faded menu with nothing behind it: the page shows through it crisp and
3721
- * bright, so the popup stops reading as a raised surface at all. (This is a DIFFERENT
3722
- * defect from the reported "the background-job list is under the conversation" one,
3723
- * which is a stacking-context injury the row takes with its own frost — see the
3724
- * `z-index` note on `HEADER_BAR_RULE`. Both were live at once on `0.2.0-rc.1`.)
3725
- * `none` stays correct for the exempt group: `applyExemptDefaults` pins those
3726
- * surfaces at alpha 1 alongside the cleared frost, so they are opaque by design.
3799
+ * The host declares its own menu frost on `body` (`ui-theme/src/styles/gradient-shadow-text.css`
3800
+ * — `--dsw-menu-backdrop-filter: blur(40px) saturate(150%)`, no media query), and every menu
3801
+ * frosts from that INHERITED value, whether it filters itself (the background-job `<ul>`) or
3802
+ * hands the value to a `MenuSurface` `.material` child. An element's own declaration always
3803
+ * beats what it inherits, so writing the token with a `none` fallback does not "add no blur"
3804
+ * at slider 0 — it DELETES the host's frost.
3727
3805
  *
3728
- * `:where()` keeps the gate free: this plugin's menu cascade is ORDERED (card group →
3729
- * header group → exemptions) at equal specificity, and a class prefix with real
3730
- * specificity would have lifted the card group over the exemption it must lose to. */
3731
- const CARD_FROST_GATE = "dab-card-frost";
3732
- const HEADER_FROST_GATE = "dab-header-frost";
3733
- /** Prefix every arm of a selector list with a `:where()` gate. A joined list is one
3734
- * selector per entry, so prefixing the string once would gate only the first arm and
3735
- * leave the rest always on. */
3736
- function gated(gate, selectors) {
3737
- return selectors.split(",").map((sel) => `:where(.${gate}) ${sel}`).join(",");
3738
- }
3806
+ * 0.3.7 answered that with a `<html>` class gate (`dab-card-frost` / `dab-header-frost`) that
3807
+ * only let the declaration through while the slider was non-zero, so 0 meant "leave the host
3808
+ * alone". That was the wrong trade and this is the report that says so: with the gate on, the
3809
+ * slider's LOW end is its HIGH end — 0 renders the host's 40 px frost and 1 renders 1 px — so
3810
+ * the control reads as dead in both directions, and 「把模糊关掉」 is simply not reachable
3811
+ * (measured in `dsh web` with 顶栏选项 at 0/0: the open-in-app panel still came out a soft,
3812
+ * opaque-looking card, because what it was showing was the host's blur, not the plugin's).
3813
+ * The token is therefore asserted at every value, `none` included: **0 means no frost**.
3814
+ * Nothing loses the host look by accident, because the part DEFAULTS are 30 px — a stock
3815
+ * install is frosted like the host's and only an explicit drag to 0 clears it. The same is
3816
+ * already true of every other blur slider in this plugin (0 = off), which is the interface
3817
+ * consistency the gate broke.
3818
+ *
3819
+ * `none` stays correct for the exempt group as well: `applyExemptDefaults` pins those surfaces
3820
+ * at alpha 1 alongside the cleared frost, so they are opaque by design. */
3739
3821
  /** Declare EVERY token this release's menus can paint their fill from with the same
3740
3822
  * value. One slider, one value, every layer reached — and a token the host does not
3741
3823
  * read is inert, so this is safe to emit on a line that only needs one.
@@ -3749,7 +3831,7 @@ window.__ModuleLoader__.load({
3749
3831
  return hostAdapter().menus.fillTokens.map((name) => `${name}:${value}`).join(";");
3750
3832
  }
3751
3833
  function popoverBlurRule() {
3752
- return `${gated(CARD_FROST_GATE, `${POPOVER_MENU_SEL},${MENU_MATERIAL_SEL}`)}{${menuFrost(CARD_BLUR_VAR)}}${POPOVER_OTHER_SEL}{${elementFrostDecl(`var(${CARD_BLUR_VAR},none)`)}}`;
3834
+ return `${POPOVER_MENU_SEL},${MENU_MATERIAL_SEL}{${menuFrost(CARD_BLUR_VAR)}}${POPOVER_OTHER_SEL}{${elementFrostDecl(`var(${CARD_BLUR_VAR},none)`)}}`;
3753
3835
  }
3754
3836
  const INPUT_FROST_CLASS = "dab-input-frost";
3755
3837
  const TAKEOVER_FROST_CLASS = "dab-takeover-frost";
@@ -3802,6 +3884,8 @@ window.__ModuleLoader__.load({
3802
3884
  }
3803
3885
  const MOBILE_HEADER_RULE = ".dsh-mobile-app-header{background:var(--dsh-any-op-bg,transparent)!important;backdrop-filter:var(--dsh-any-part-blur-global,none);-webkit-backdrop-filter:var(--dsh-any-part-blur-global,none)}";
3804
3886
  const HEADER_BAR_CLASS = "dab-header-bar";
3887
+ /** Added to the same row only while its blur is non-zero — see `HEADER_BAR_RULE`. */
3888
+ const HEADER_BAR_FROST_CLASS = "dab-header-bar-frost";
3805
3889
  const HEADER_BAR_ANCHOR = "[data-slot^=\"conversation.session.header\"]";
3806
3890
  /** The session header bar, or null where this host has no such element.
3807
3891
  *
@@ -3819,13 +3903,35 @@ window.__ModuleLoader__.load({
3819
3903
  if (bar.className.includes("headerBlank")) return null;
3820
3904
  return bar;
3821
3905
  }
3822
- /** Exactly one owner: the row the current conversation mounted. A row that went
3823
- * blank, or one the host replaced, must not keep a class from an earlier pass. */
3824
3906
  let headerBarEl = null;
3825
- function claimHeaderBar(bar) {
3826
- if (headerBarEl !== null && headerBarEl !== bar) headerBarEl.classList.remove(HEADER_BAR_CLASS);
3907
+ /** Exactly one owner: the row the current conversation mounted. A row that went
3908
+ * blank, or one the host replaced, must not keep a class — or a `position` — from an
3909
+ * earlier pass. `frost` decides whether the row also carries the underlay arm, and the
3910
+ * underlay's containing block is written inline only while it is needed. */
3911
+ function releaseHeaderBar(bar) {
3912
+ bar.classList.remove(HEADER_BAR_CLASS, HEADER_BAR_FROST_CLASS);
3913
+ if (bar.getAttribute(TAKEOVER_POS_ATTR) === "1") {
3914
+ bar.style.removeProperty("position");
3915
+ bar.removeAttribute(TAKEOVER_POS_ATTR);
3916
+ }
3917
+ }
3918
+ function claimHeaderBar(bar, frost) {
3919
+ if (headerBarEl !== null && headerBarEl !== bar) releaseHeaderBar(headerBarEl);
3920
+ if (bar === null) {
3921
+ headerBarEl = null;
3922
+ return;
3923
+ }
3827
3924
  headerBarEl = bar;
3828
- if (bar !== null) bar.classList.add(HEADER_BAR_CLASS);
3925
+ bar.classList.add(HEADER_BAR_CLASS);
3926
+ if (!frost) {
3927
+ bar.classList.remove(HEADER_BAR_FROST_CLASS);
3928
+ return;
3929
+ }
3930
+ if (getComputedStyle(bar).position === "static") {
3931
+ bar.style.position = "relative";
3932
+ bar.setAttribute(TAKEOVER_POS_ATTR, "1");
3933
+ }
3934
+ bar.classList.add(HEADER_BAR_FROST_CLASS);
3829
3935
  }
3830
3936
  /** The bar's paint. The variables are written by `applyHeaderBar`; the class is
3831
3937
  * only ever added when they are, so a host the plugin is not theming keeps its
@@ -3865,12 +3971,27 @@ window.__ModuleLoader__.load({
3865
3971
  * filter never had to be blamed for its anchoring). 10 sits in that gap: above 9, far
3866
3972
  * below 100, so a portaled list still covers the title row exactly as the host intends.
3867
3973
  *
3868
- * `z-index` needs no `position` here and none is written: the row is a flex ITEM of
3869
- * `ConversationRoot .root{display:flex;flex-direction:column}` on every release in range,
3870
- * and z-index applies to flex items as they are. Writing `position:relative` would also
3871
- * make the row a containing block — and deciding a host box's positioning is the exact
3872
- * race this plugin lost once already (issue #24). */
3873
- const HEADER_BAR_RULE = `.${HEADER_BAR_CLASS}{background:var(--dsh-any-op-header-bar,transparent)!important;backdrop-filter:var(--dsh-any-blur-header-bar,none);-webkit-backdrop-filter:var(--dsh-any-blur-header-bar,none);z-index:10}`;
3974
+ * `z-index` needs no `position` here and none is written by the BASE rule: the row is
3975
+ * a flex ITEM of `ConversationRoot .root{display:flex;flex-direction:column}` on every
3976
+ * release in range, and z-index applies to flex items as they are. Writing
3977
+ * `position:relative` would also make the row a containing block — and deciding a host
3978
+ * box's positioning is the exact race this plugin lost once already (issue #24).
3979
+ *
3980
+ * THE FROST DOES NOT SIT ON THE ROW. That was the first version of this rule, and it
3981
+ * cost the row's own children their backdrop: `backdrop-filter` makes an element a
3982
+ * BACKDROP ROOT as well as a stacking context, so an in-place header popover (the
3983
+ * session-log `…` menu, which `HeaderAction.tsx` renders without `portal`) could only
3984
+ * ever blur what the row itself paints — a flat tint — and the 「标题栏」 blur slider
3985
+ * read as dead on exactly the panel that hangs off the row. `isolation:isolate` is a
3986
+ * stacking context WITHOUT being a backdrop root, so the frost now rides a `::before`
3987
+ * underlay like it does on every other host part this plugin filters (`PART_BLUR_RULE`,
3988
+ * `inputBlurRule`), and the row's children sample the wallpaper again. The underlay
3989
+ * needs the row to be its containing block, which is why the frost arm adds
3990
+ * `position:relative` — INLINE, only when the host left the row `static`, and marked
3991
+ * `data-dab-pos-patched` so only this writer takes it back: the same guard
3992
+ * `setBlur` uses, for the same reason (a stylesheet `position` would cascade against
3993
+ * whatever the host or another plugin decides that box's position is). */
3994
+ const HEADER_BAR_RULE = `.${HEADER_BAR_CLASS}{background:var(--dsh-any-op-header-bar,transparent)!important;z-index:10}.${HEADER_BAR_FROST_CLASS}{isolation:isolate}.${HEADER_BAR_FROST_CLASS}::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;-webkit-backdrop-filter:var(--dsh-any-blur-header-bar,none);backdrop-filter:var(--dsh-any-blur-header-bar,none)}`;
3874
3995
  /** The 「标题栏」card's paint: the card's material at the card's opacity, empty
3875
3996
  * where the plugin is not theming. The conversation header row, the Windows
3876
3997
  * caption band, and — through the desktop preload's probe — the native
@@ -3889,7 +4010,7 @@ window.__ModuleLoader__.load({
3889
4010
  const bar = discoverHeaderBar();
3890
4011
  const paint = headerBarPaint(src);
3891
4012
  if (paint === "") {
3892
- claimHeaderBar(null);
4013
+ claimHeaderBar(null, false);
3893
4014
  root.style.removeProperty("--dsh-any-op-header-bar");
3894
4015
  root.style.removeProperty("--dsh-any-blur-header-bar");
3895
4016
  applyNativeCaption(surfaces);
@@ -3899,7 +4020,7 @@ window.__ModuleLoader__.load({
3899
4020
  const px = rBlurs().headerBar;
3900
4021
  if (px > 0) root.style.setProperty("--dsh-any-blur-header-bar", `blur(${px}px)`);
3901
4022
  else root.style.removeProperty("--dsh-any-blur-header-bar");
3902
- claimHeaderBar(bar);
4023
+ claimHeaderBar(bar, px > 0);
3903
4024
  applyNativeCaption(surfaces);
3904
4025
  }
3905
4026
  const CAPTION_FRAME_CLASS = "dab-frame-caption";
@@ -3911,7 +4032,7 @@ window.__ModuleLoader__.load({
3911
4032
  const CAPTION_INK_ATTR = "data-dab-caption-ink";
3912
4033
  /** The strip's chrome ink, and its collapsed-state variant.
3913
4034
  *
3914
- * WHY THIS IS NOT THE LABEL INK: the plugin flips the label tokens against the
4035
+ * WHY THIS IS NOT SIMPLY THE LABEL INK: the plugin flips the label tokens against the
3915
4036
  * PICKED PALETTE (white on a dark pick), but the caption band is that palette at
3916
4037
  * the sidebar/header-bar opacity OVER the wallpaper. At a low opacity the strip
3917
4038
  * is mostly wallpaper, so white ink lands on a light band and the toggle
@@ -3920,11 +4041,31 @@ window.__ModuleLoader__.load({
3920
4041
  * painted on: past ~85% opacity the surface dominates and the palette direction
3921
4042
  * is the truth; below it the wallpaper's brightness verdict decides.
3922
4043
  *
4044
+ * WHY THAT DERIVATION IS NOT THE WHOLE ANSWER (issue #30): the same derivation makes
4045
+ * the native caption menubar (「应用 / 编辑」) black on a dark theme whose sidebar is
4046
+ * translucent over a bright wallpaper — the strip really is light there, so the strip
4047
+ * is right and the rest of the window is wrong, and the two reports of this same
4048
+ * situation asked for opposite outcomes. So the policy is the user's: 「标题栏文字」
4049
+ * picks the winner, and `auto` keeps the strip-following behaviour that fixed the
4050
+ * invisible toggle. `theme` returns null, which removes every ink declaration this
4051
+ * function feeds — the menubar and the sidebar's fixed controls then read the host's
4052
+ * own label tokens, exactly as they did before 0.3.8.
4053
+ *
3923
4054
  * Returned as `{ ink, collapsedInk }` because the strip's material changes with
3924
4055
  * the sidebar: expanded it is the sidebar segment, collapsed it is the 「标题栏」
3925
- * card's band. `null` means "no verdict and no palette direction" — leave the
3926
- * host's ink alone. */
4056
+ * card's band. `null` means "no override" — either no verdict and no palette
4057
+ * direction, or the user asked for the theme's own ink. */
3927
4058
  function captionInkPair() {
4059
+ const mode = rCaptionInk();
4060
+ if (mode === "theme") return null;
4061
+ if (mode === "white") return {
4062
+ ink: "#fff",
4063
+ collapsedInk: "#fff"
4064
+ };
4065
+ if (mode === "black") return {
4066
+ ink: "#000",
4067
+ collapsedInk: "#000"
4068
+ };
3928
4069
  const verdict = rBgDark();
3929
4070
  const inkIsWhite = rSchemeOverride() === "auto" ? rHasColor() ? rColorScheme() === "dark" : verdict : rScheme() === "dark";
3930
4071
  if (inkIsWhite === null || inkIsWhite === void 0) return null;
@@ -4030,7 +4171,7 @@ window.__ModuleLoader__.load({
4030
4171
  const fill = captionFill(src);
4031
4172
  const paint = headerBarPaint(src) || fill;
4032
4173
  const width = "var(--dsh-windows-sidebar-width,0px)";
4033
- const css = fill === "" ? "" : `${WCO_PROBE_SEL}{--dsw-specific-sidebar-fill:transparent}[data-windows-titlebar] .${CAPTION_FRAME_CLASS}::before{background:var(--dsh-any-op-header-bar,${paint})!important;backdrop-filter:var(--dsh-any-blur-header-bar,none);-webkit-backdrop-filter:var(--dsh-any-blur-header-bar,none);clip-path:inset(0 0 0 ${width});-webkit-clip-path:inset(0 0 0 ${width})}[data-windows-titlebar] .${CAPTION_FRAME_CLASS}::after{content:"";position:absolute;top:0;left:0;pointer-events:none;z-index:-1;width:${width};height:var(--dsh-windows-titlebar-height,40px);background:var(--dsh-any-op-sidebar,${fill});backdrop-filter:var(--dsh-any-blur-sidebar-band,none);-webkit-backdrop-filter:var(--dsh-any-blur-sidebar-band,none)}[data-windows-titlebar]:has([role="dialog"][aria-modal="true"]) .${CAPTION_FRAME_CLASS}::before,[data-windows-titlebar]:has([role="dialog"][aria-modal="true"]) .${CAPTION_FRAME_CLASS}::after{box-shadow:inset 0 0 0 9999px var(--dsw-alias-bg-mask-1,rgba(0,0,0,.24))}:root:has([data-sidebar-collapsed="true"]){--dsh-any-caption-ink:var(--dsh-any-caption-ink-collapsed,#fff)!important}[data-windows-titlebar] [data-windows-menu]{--dsw-alias-label-secondary:var(--dsh-any-caption-ink,#fff);--dsw-alias-label-primary:var(--dsh-any-caption-ink,#fff)}[data-windows-titlebar] .${FLAT_CENTER_CLASS}{border-top-left-radius:0!important}`;
4174
+ const css = fill === "" ? "" : `${WCO_PROBE_SEL}{--dsw-specific-sidebar-fill:transparent}[data-windows-titlebar] .${CAPTION_FRAME_CLASS}::before{background:var(--dsh-any-op-header-bar,${paint})!important;backdrop-filter:var(--dsh-any-blur-header-bar,none);-webkit-backdrop-filter:var(--dsh-any-blur-header-bar,none);clip-path:inset(0 0 0 ${width});-webkit-clip-path:inset(0 0 0 ${width})}[data-windows-titlebar] .${CAPTION_FRAME_CLASS}::after{content:"";position:absolute;top:0;left:0;pointer-events:none;z-index:-1;width:${width};height:var(--dsh-windows-titlebar-height,40px);background:var(--dsh-any-op-sidebar,${fill});backdrop-filter:var(--dsh-any-blur-sidebar-band,none);-webkit-backdrop-filter:var(--dsh-any-blur-sidebar-band,none)}[data-windows-titlebar]:has([role="dialog"][aria-modal="true"]) .${CAPTION_FRAME_CLASS}::before,[data-windows-titlebar]:has([role="dialog"][aria-modal="true"]) .${CAPTION_FRAME_CLASS}::after{box-shadow:inset 0 0 0 9999px var(--dsw-alias-bg-mask-1,rgba(0,0,0,.24))}` + (rCaptionInk() === "theme" ? "" : ":root:has([data-sidebar-collapsed=\"true\"]){--dsh-any-caption-ink:var(--dsh-any-caption-ink-collapsed,#fff)!important}[data-windows-titlebar] [data-windows-menu]{--dsw-alias-label-secondary:var(--dsh-any-caption-ink,#fff);--dsw-alias-label-primary:var(--dsh-any-caption-ink,#fff)}") + `[data-windows-titlebar] .${FLAT_CENTER_CLASS}{border-top-left-radius:0!important}`;
4034
4175
  const el = ensureWcoStyle();
4035
4176
  if (el.textContent !== css) el.textContent = css;
4036
4177
  const ink = css === "" ? null : captionInkPair();
@@ -4102,7 +4243,7 @@ window.__ModuleLoader__.load({
4102
4243
  * backdrop root each of those files documents avoiding. */
4103
4244
  const HEADER_BLUR_VAR = "--dsh-any-blur-header";
4104
4245
  function headerPopoverRule() {
4105
- return `${gated(HEADER_FROST_GATE, HEADER_SURFACES_PADDED.join(","))}{${menuFrost(HEADER_BLUR_VAR)}}`;
4246
+ return `${HEADER_SURFACES_PADDED.join(",")}{${menuFrost(HEADER_BLUR_VAR)}}`;
4106
4247
  }
4107
4248
  function exemptDefaultRule() {
4108
4249
  return `[role="dialog"][aria-modal="true"]:not([aria-labelledby]){backdrop-filter:none;-webkit-backdrop-filter:none}[role="menu"]:has([class*="_danger"]){${hostAdapter().menus.frostDecl("none")};-webkit-text-stroke-width:0}`;
@@ -4116,13 +4257,8 @@ window.__ModuleLoader__.load({
4116
4257
  function applyHeaderPopovers() {
4117
4258
  const root = document.documentElement;
4118
4259
  const px = rBlurs().header;
4119
- if (px > 0) {
4120
- root.style.setProperty(HEADER_BLUR_VAR, `blur(${px}px)`);
4121
- root.classList.add(HEADER_FROST_GATE);
4122
- } else {
4123
- root.style.removeProperty(HEADER_BLUR_VAR);
4124
- root.classList.remove(HEADER_FROST_GATE);
4125
- }
4260
+ if (px > 0) root.style.setProperty(HEADER_BLUR_VAR, `blur(${px}px)`);
4261
+ else root.style.removeProperty(HEADER_BLUR_VAR);
4126
4262
  let opacity = rHeaderOpacity();
4127
4263
  if (opacity < 0) opacity = 0;
4128
4264
  if (opacity > 1) opacity = 1;
@@ -4690,17 +4826,13 @@ window.__ModuleLoader__.load({
4690
4826
  /** Blur of the option panels inside the settings dialog (.dab-card), owned by
4691
4827
  * the "dialog option panel" (card) blur slider. Written as a plugin-owned
4692
4828
  * variable consumed by SETTINGS_STYLE_RULE — deliberately NOT applied to the
4693
- * homepage center column, which this slider must never touch. The gate class is
4694
- * what hands the menu frost token over at all (see `CARD_FROST_GATE`). */
4829
+ * homepage center column, which this slider must never touch. At 0 the variable is
4830
+ * removed and the menu rule's `none` fallback wins, which is what "no frost" means
4831
+ * on this slider (see the note above `popoverBlurRule`). */
4695
4832
  function applyCardPanelsBlur(px) {
4696
4833
  const root = document.documentElement;
4697
- if (px > 0) {
4698
- root.style.setProperty(CARD_BLUR_VAR, `blur(${px}px)`);
4699
- root.classList.add(CARD_FROST_GATE);
4700
- } else {
4701
- root.style.removeProperty(CARD_BLUR_VAR);
4702
- root.classList.remove(CARD_FROST_GATE);
4703
- }
4834
+ if (px > 0) root.style.setProperty(CARD_BLUR_VAR, `blur(${px}px)`);
4835
+ else root.style.removeProperty(CARD_BLUR_VAR);
4704
4836
  }
4705
4837
  /** Apply per-part interface blur to the AppFrame columns + settings panel. */
4706
4838
  function applyPartBlurs(blurs) {
@@ -5431,7 +5563,7 @@ window.__ModuleLoader__.load({
5431
5563
  const name = set[k];
5432
5564
  if (name) document.documentElement.style.removeProperty(name);
5433
5565
  }
5434
- document.documentElement.classList.remove(INPUT_FROST_CLASS, CARD_FROST_GATE, HEADER_FROST_GATE);
5566
+ document.documentElement.classList.remove(INPUT_FROST_CLASS);
5435
5567
  applyTakeoverFrost(0);
5436
5568
  document.documentElement.style.removeProperty("--dsh-any-op-header-bar");
5437
5569
  document.documentElement.style.removeProperty("--dsh-any-blur-header-bar");
@@ -5439,7 +5571,7 @@ window.__ModuleLoader__.load({
5439
5571
  document.documentElement.style.removeProperty("--dsh-any-caption-ink");
5440
5572
  document.documentElement.style.removeProperty("--dsh-any-caption-ink-collapsed");
5441
5573
  releaseCaptionInk();
5442
- document.querySelectorAll(`.${HEADER_BAR_CLASS}`).forEach((el) => el.classList.remove(HEADER_BAR_CLASS));
5574
+ document.querySelectorAll(`.${HEADER_BAR_CLASS}`).forEach((el) => releaseHeaderBar(el));
5443
5575
  headerBarEl = null;
5444
5576
  frameEl?.classList.remove(CAPTION_FRAME_CLASS);
5445
5577
  document.querySelectorAll(`.${FLAT_CENTER_CLASS}`).forEach((el) => el.classList.remove(FLAT_CENTER_CLASS));
@@ -7464,6 +7596,47 @@ window.__ModuleLoader__.load({
7464
7596
  blurs[blurKey] = v;
7465
7597
  setBlurs(blurs);
7466
7598
  }
7599
+ }),
7600
+ isHeaderBar && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7601
+ className: "dab-slider-block",
7602
+ title: t("captionInkHint"),
7603
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7604
+ className: "dab-slider-label",
7605
+ children: t("captionInkLabel")
7606
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7607
+ style: {
7608
+ display: "flex",
7609
+ alignItems: "center",
7610
+ gap: 10
7611
+ },
7612
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
7613
+ className: "dab-select",
7614
+ value: rCaptionInk(),
7615
+ onChange: (e) => {
7616
+ cfg.captionInk = e.target.value;
7617
+ applyHeaderBar();
7618
+ saveConfig();
7619
+ },
7620
+ children: [
7621
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
7622
+ value: "auto",
7623
+ children: t("captionInkAuto")
7624
+ }),
7625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
7626
+ value: "theme",
7627
+ children: t("captionInkTheme")
7628
+ }),
7629
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
7630
+ value: "white",
7631
+ children: t("captionInkWhite")
7632
+ }),
7633
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
7634
+ value: "black",
7635
+ children: t("captionInkBlack")
7636
+ })
7637
+ ]
7638
+ })
7639
+ })]
7467
7640
  })
7468
7641
  ]
7469
7642
  }, blurKey);