dsh-quick-toc 0.9.5 → 0.10.1

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
@@ -44,6 +44,11 @@ window.__ModuleLoader__.load({
44
44
  var Z_PANEL = 14; // the docked panel
45
45
  var Z_SHEET = 16; // the curtain, which replaces the docked panel while it is up
46
46
  var Z_HOVER = 18; // hover preview card: above both, still inside the free band
47
+ // Corner radius of the two surfaces. One number each so the panel, its glass
48
+ // twin and its shadow twin can never disagree, and so the curtain's bottom
49
+ // pair stays in step with the panel's rounded shape.
50
+ var RADIUS_PANEL = 18;
51
+ var RADIUS_SHEET = 18;
47
52
  // A narrow conversation column has no room for a handle floating over the text: below
48
53
  // NARROW_CONV_W the collapsed-panel handle slides back into the divider (its usual
49
54
  // motion) while the droplet above the conversation stays as the way in. Nothing opens
@@ -72,6 +77,16 @@ window.__ModuleLoader__.load({
72
77
  // two "fight" on long group headers, the label's box running right up to the
73
78
  // circle.
74
79
  var END_ZONE = 32;
80
+ // The bookmark ribbon's gutter at the left of every outline row. Reserved ALWAYS
81
+ // (whether or not anything is bookmarked) so that bookmarking a row never shifts
82
+ // the text: a ribbon that appeared by pushing content sideways would make the
83
+ // whole list twitch every time something is saved.
84
+ //
85
+ // 16px wide on purpose: at 9px the notch ate most of the shape and the ribbon read
86
+ // as a left-pointing arrow. At 16px the flat body dominates and the cut edge reads
87
+ // as a bookmark tab.
88
+ var MARK_W = 16;
89
+ var MARK_INSET = 20;
75
90
  // How long the collapsed edge handle takes to slip back into the edge when it is
76
91
  // the thing that was clicked. The panel's own slide waits exactly this long, so the
77
92
  // two motions are sequential instead of crossing over.
@@ -144,6 +159,7 @@ window.__ModuleLoader__.load({
144
159
  "search.hintOldest": "已经是最早的消息",
145
160
  "hint.autoOff": "上面还有更早的历史(自动加载已关闭,点「未加载」的条目才会加载)",
146
161
  "hint.bottom": "已经到底了",
162
+ "outline.empty": "这个会话还没有可跳转的标题。等对话里出现 Markdown 标题,这里就会列出来。",
147
163
  "outline.bottom": "回到底部",
148
164
  "outline.backToMine": "回到我滚到的位置",
149
165
  "turn.jumpReply": "跳转到该回合的模型回答开头",
@@ -160,6 +176,15 @@ window.__ModuleLoader__.load({
160
176
  "preview.truncated": "预览由宿主提供,可能被截断",
161
177
  "preview.pin": "钉住预览",
162
178
  "preview.unpin": "取消钉住",
179
+ "marks.tip.only": "只看书签({n})",
180
+ "marks.tip.all": "显示全部大纲(书签 {n})",
181
+ "marks.empty": "还没有书签。右键任意条目,选「加入书签」即可。",
182
+ "menu.goto": "跳到这一节",
183
+ "menu.mark": "加入书签",
184
+ "menu.unmark": "移出书签",
185
+ "menu.copyTitle": "复制标题",
186
+ "menu.copied": "已复制",
187
+ "menu.copySection": "复制本节大纲",
163
188
  "menu.pin": "钉住预览",
164
189
  "menu.unpin": "取消钉住",
165
190
  "menu.end": "跳到本节末尾",
@@ -179,6 +204,19 @@ window.__ModuleLoader__.load({
179
204
  "settings.density": "大纲密度",
180
205
  "settings.density.comfy": "标准",
181
206
  "settings.density.compact": "紧凑",
207
+ "sheet.resize": "拖动调整幕布高度",
208
+ "settings.glass": "半透明界面",
209
+ "settings.glass.autoOff": "已根据当前设备的帧率自动关闭,可手动重新开启。",
210
+ "settings.glass.tip": "开启后幕布会模糊它后面的对话,面板也能透出背景;关闭则改为不透明。设备性能不足时会自动关闭。",
211
+ "settings.glassStrength": "通透度",
212
+ "settings.glassStrength.tip": "数值越大越透。面板与幕布一起调整,面板会比幕布更实一些。",
213
+ "settings.accent": "主题色",
214
+ "settings.accent.blue": "蓝色",
215
+ "settings.accent.red": "红色",
216
+ "settings.accent.yellow": "黄色",
217
+ "settings.accent.green": "绿色",
218
+ "settings.accent.purple": "紫色",
219
+ "settings.accent.pink": "粉色",
182
220
  "settings.zoom": "面板缩放",
183
221
  "settings.zoom.tip": "文字、图标与按钮按比例缩放,面板本身的尺寸不变",
184
222
  "settings.sheetZoom": "幕布缩放",
@@ -249,6 +287,7 @@ window.__ModuleLoader__.load({
249
287
  "search.hintOldest": "This is the earliest message",
250
288
  "hint.autoOff": "There is older history above (auto-loading is off — click an unloaded row to load it)",
251
289
  "hint.bottom": "You have reached the end",
290
+ "outline.empty": "Nothing to jump to in this conversation yet. Markdown headings will be listed here as they appear.",
252
291
  "outline.bottom": "Back to the newest entry",
253
292
  "outline.backToMine": "Back to where I scrolled to",
254
293
  "turn.jumpReply": "Jump to the start of this turn's model reply",
@@ -267,6 +306,15 @@ window.__ModuleLoader__.load({
267
306
  "preview.truncated": "Preview comes from the host and may be truncated",
268
307
  "preview.pin": "Pin this preview",
269
308
  "preview.unpin": "Unpin",
309
+ "marks.tip.only": "Show bookmarks only ({n})",
310
+ "marks.tip.all": "Show the whole outline ({n} bookmarked)",
311
+ "marks.empty": "No bookmarks yet. Right-click any entry and choose \u201cAdd bookmark\u201d.",
312
+ "menu.goto": "Jump to this section",
313
+ "menu.mark": "Add bookmark",
314
+ "menu.unmark": "Remove bookmark",
315
+ "menu.copyTitle": "Copy heading",
316
+ "menu.copied": "Copied",
317
+ "menu.copySection": "Copy this section",
270
318
  "menu.pin": "Pin this preview",
271
319
  "menu.unpin": "Unpin this preview",
272
320
  "menu.end": "Jump to the end of this section",
@@ -286,6 +334,19 @@ window.__ModuleLoader__.load({
286
334
  "settings.density": "Outline density",
287
335
  "settings.density.comfy": "Comfy",
288
336
  "settings.density.compact": "Compact",
337
+ "sheet.resize": "Drag to resize the curtain",
338
+ "settings.glass": "Translucent surfaces",
339
+ "settings.glass.autoOff": "Switched off automatically for this device's frame rate; you can turn it back on.",
340
+ "settings.glass.tip": "The curtain blurs the conversation behind it and the panel lets the background show through; off makes both solid. Turned off automatically on a slow device.",
341
+ "settings.glassStrength": "See-through",
342
+ "settings.glassStrength.tip": "Higher is lighter. One setting for both surfaces; the panel always stays a little denser than the curtain.",
343
+ "settings.accent": "Theme colour",
344
+ "settings.accent.blue": "Blue",
345
+ "settings.accent.red": "Red",
346
+ "settings.accent.yellow": "Yellow",
347
+ "settings.accent.green": "Green",
348
+ "settings.accent.purple": "Purple",
349
+ "settings.accent.pink": "Pink",
289
350
  "settings.zoom": "Panel scale",
290
351
  "settings.zoom.tip": "Scales the text, icons and buttons; the panel's own size is unchanged",
291
352
  "settings.sheetZoom": "Curtain scale",
@@ -370,12 +431,14 @@ window.__ModuleLoader__.load({
370
431
  // purpose — they describe THIS screen, not the account.
371
432
  var NAMESPACE = "dsh-quick-toc";
372
433
  // Fields backed by the Host settings document; everything else is localStorage.
373
- var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, density: 1, zoom: 1, sheetZoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1, autoLoad: 1 };
434
+ var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, density: 1, accent: 1, glass: 1, glassStrength: 1, sheetHeight: 1, zoom: 1, sheetZoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1, autoLoad: 1 };
374
435
  // The schema defaults (mirrored here for the card's per-field reset and for the
375
436
  // one-time import of a 0.5.x install's stored values).
376
437
  var DEFAULTS = {
377
438
  dock: "left", y: 0, w: 0, h: 0,
378
- levels: [1, 2, 3, 4, 5, 6], density: "comfy",
439
+ // "share" of see-through: 0 = nearly solid, 100 = the lightest material
440
+ levels: [1, 2, 3, 4, 5, 6], density: "comfy", accent: "blue",
441
+ glass: true, glassStrength: 60, sheetHeight: 1,
379
442
  zoom: 1, sheetZoom: 1, handle: 0.5,
380
443
  fuzzy: false, hover: true, lang: "auto", debug: false,
381
444
  remember: true, autoLoad: true
@@ -387,6 +450,10 @@ window.__ModuleLoader__.load({
387
450
  h: "dsh-quick-toc.panelH.v1",
388
451
  levels: "dsh-quick-toc.levels.v1",
389
452
  density: "dsh-quick-toc.density.v1",
453
+ accent: "dsh-quick-toc.accent.v1",
454
+ glass: "dsh-quick-toc.glass.v1",
455
+ glassStrength: "dsh-quick-toc.glassStrength.v1",
456
+ sheetHeight: "dsh-quick-toc.sheetHeight.v1",
390
457
  zoom: "dsh-quick-toc.zoom.v1",
391
458
  sheetZoom: "dsh-quick-toc.sheetZoom.v1",
392
459
  handle: "dsh-quick-toc.handle.v1",
@@ -397,6 +464,117 @@ window.__ModuleLoader__.load({
397
464
  autoLoad: "dsh-quick-toc.autoLoad.v1",
398
465
  debug: "dsh-quick-toc.debug"
399
466
  };
467
+ // Theme colours for every "selected / active / current" surface. Blue is the
468
+ // null entry: today's exact values (host brand text + fixed fills), so the
469
+ // default reproduces the current look pixel for pixel. The rest pair a fill
470
+ // triplet with a solid that stays legible on both light and dark surfaces
471
+ // (the dot shows the vivid hue; the solid is the readable one).
472
+ var ACCENTS = {
473
+ blue: { rgb: "79, 140, 255", solid: null, dot: "#4f8cff" },
474
+ red: { rgb: "245, 72, 77", solid: "#F5484D", dot: "#F5484D" },
475
+ yellow: { rgb: "232, 162, 12", solid: "#D99300", dot: "#FFB020" },
476
+ green: { rgb: "48, 164, 108", solid: "#2FA36B", dot: "#2FA36B" },
477
+ purple: { rgb: "142, 78, 198", solid: "#8E4EC6", dot: "#8E4EC6" },
478
+ pink: { rgb: "233, 61, 130", solid: "#E93D82", dot: "#E93D82" }
479
+ };
480
+ // self-contained on purpose: this runs in scopes where the store's own
481
+ // hasOwn helper is not visible, so it must not borrow it (borrowing it
482
+ // threw on every render and the error boundary ate the whole header)
483
+ var normAccent = function (v) { return (typeof v === "string" && Object.prototype.hasOwnProperty.call(ACCENTS, v)) ? v : "blue"; };
484
+ // 0..100 whole numbers — the "how see-through" slider
485
+ var clampPct = function (raw, fallback) {
486
+ return raw !== null && raw !== undefined && raw !== "" && isFinite(Number(raw))
487
+ ? Math.min(100, Math.max(0, Math.round(Number(raw)))) : fallback;
488
+ };
489
+ // 0.25..1, two decimals — the curtain's own height as a share of the area
490
+ var clampFrac = function (raw, fallback) {
491
+ return raw !== null && raw !== undefined && raw !== "" && isFinite(Number(raw))
492
+ ? Math.min(1, Math.max(0.25, Math.round(Number(raw) * 100) / 100)) : fallback;
493
+ };
494
+ // The three theme-dependent rules of the once-injected stylesheet, rebuilt
495
+ // whenever the theme colour changes. A later style element wins over the
496
+ // injected base, which keeps today's values as its fallback.
497
+ var accentCssText = function (id) {
498
+ var t = ACCENTS[normAccent(id)] || ACCENTS.blue;
499
+ var solid = t.solid || "var(--dsw-alias-brand-primary, #4f8cff)";
500
+ return ".dqt-nav{box-shadow:inset 0 0 0 1px " + solid + ";background:rgba(" + t.rgb + ",0.10)}"
501
+ + ".dqt-phead:focus-visible{outline-color:" + solid + "}"
502
+ + ".dqt-slider{accent-color:" + solid + "}";
503
+ };
504
+ var applyAccentCss = function (id) {
505
+ if (typeof document === "undefined") return;
506
+ var el = document.getElementById("dsh-quick-toc-accent");
507
+ if (!el) {
508
+ el = document.createElement("style");
509
+ el.id = "dsh-quick-toc-accent";
510
+ document.head.appendChild(el);
511
+ }
512
+ el.textContent = accentCssText(id);
513
+ };
514
+ // A caller-owned palette for one render: blue shares the module palette
515
+ // object itself (default path: zero behaviour change, same identity), any
516
+ // other choice merges the same keys with its own triplet.
517
+ var themeC = function (id) {
518
+ var key = normAccent(id);
519
+ if (key === "blue") return C;
520
+ var t = ACCENTS[key];
521
+ var out = {};
522
+ for (var k in C) if (Object.prototype.hasOwnProperty.call(C, k)) out[k] = C[k];
523
+ out.accent = t.solid;
524
+ out.accentRgb = t.rgb;
525
+ out.chip = "rgba(" + t.rgb + ", 0.18)";
526
+ out.groupTint = "rgba(" + t.rgb + ", 0.10)";
527
+ out.groupEdge = "rgba(" + t.rgb + ", 0.85)";
528
+ out.groupEdgeSoft = "rgba(" + t.rgb + ", 0.35)";
529
+ return out;
530
+ };
531
+ // How see-through the material is, from the reader's 0..100 preference.
532
+ // One slider drives both surfaces, each with its own range: the panel is read
533
+ // from, so it stays denser; the curtain is transient, so it goes lighter.
534
+ var glassPct = function (strength, forSheet) {
535
+ var s = Math.min(100, Math.max(0, Number(strength)));
536
+ if (!isFinite(s)) s = 60;
537
+ // lighter base than before: the panel has to look see-through again, and
538
+ // the card uses the same scale so the two always match
539
+ // panel/card 50 % and curtain 36 % at the default see-through of 60 — the
540
+ // densities the reader picked by eye
541
+ var pct = forSheet ? (62 - s * 0.433) : (62 - s * 0.2);
542
+ return Math.round(Math.min(96, Math.max(24, pct)));
543
+ };
544
+ // Applies the reader's glass choices to a palette, returning a fresh object.
545
+ // With glass off (or unsupported) every surface falls back to the solid panel
546
+ // colour, and the blur strings become undefined rather than empty.
547
+ // A device that cannot hold the frame rate loses the blur by itself. The
548
+ // verdict is remembered, so a slow machine pays for the probe once.
549
+ var AUTO_OFF_KEY = "dsh-quick-toc.glassAutoOff.v1";
550
+ var autoGlassOff = (function () {
551
+ try { return localStorage.getItem(AUTO_OFF_KEY) === "1"; } catch (e) { return false; }
552
+ })();
553
+ var glassC = function (base, prefs) {
554
+ var on = !!(prefs && prefs.glass) && !autoGlassOff && GLASS_OK;
555
+ var s = prefs && prefs.glassStrength !== undefined ? prefs.glassStrength : 60;
556
+ var out = {};
557
+ for (var k in base) if (Object.prototype.hasOwnProperty.call(base, k)) out[k] = base[k];
558
+ out.glassOn = on;
559
+ out.glassBg = on ? "color-mix(in srgb, var(--dsw-alias-bg-base, rgba(24, 28, 36, 0.96)) " + glassPct(s, false) + "%, transparent)" : C.panelBg;
560
+ out.sheetBg = on ? "color-mix(in srgb, var(--dsw-alias-bg-base, rgba(24, 28, 36, 0.96)) " + glassPct(s, true) + "%, transparent)" : C.panelBg;
561
+ // The preview card is a small floating layer, so it needs a lighter mix than
562
+ // the panel to show any blur at all at its size — at the panel's density the
563
+ // material reads as flat white.
564
+ out.cardBg = on
565
+ ? "color-mix(in srgb, var(--dsw-alias-bg-base, rgba(24, 28, 36, 0.96)) " + Math.max(30, glassPct(s, false) - 12) + "%, transparent)"
566
+ : C.panelBg;
567
+ // Small radius on purpose: the panel is a place you read, so the material
568
+ // should only soften the conversation, never push it toward "text smeared
569
+ // into a blur". The curtain can go a little further — it is transient.
570
+ // A very small radius on the panel on purpose: enough to soften the
571
+ // conversation, not enough to destroy the sense that there IS something
572
+ // behind it (a bigger blur made the surface read as solid). The curtain
573
+ // sits at 10px, which is the reader's own pick.
574
+ out.panelBlur = on ? "blur(3px) saturate(1.1)" : undefined;
575
+ out.sheetBlur = on ? "blur(5px) saturate(1.1)" : undefined;
576
+ return out;
577
+ };
400
578
  // Writing a field's schema default through the card IS its reset — a value that
401
579
  // equals the default carries no customization, so it must not pin an entry (and
402
580
  // the "customized" badge with it) into the user layer.
@@ -461,6 +639,44 @@ window.__ModuleLoader__.load({
461
639
  localStorage.setItem(READ_POS_KEY, JSON.stringify(map));
462
640
  } catch (e) {}
463
641
  };
642
+ // ---- bookmarks (browser-local, per session) -------------------------------
643
+ // Same shape as the reading position and for the same reason: a node key only
644
+ // means anything inside ONE session, so bookmarks are stored per session and
645
+ // never travel through the host's settings document. `{ [sessionId]: [entry] }`
646
+ // where an entry is { key, idx, title, level, at }.
647
+ var BOOKMARK_KEY = "dsh-quick-toc.bookmarks.v1";
648
+ var BOOKMARK_MAX = 300;
649
+ var bookmarkMap = function () {
650
+ try {
651
+ var raw = localStorage.getItem(BOOKMARK_KEY);
652
+ var parsed = raw ? JSON.parse(raw) : null;
653
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
654
+ } catch (e) { return {}; }
655
+ };
656
+ var bookmarksOf = function (sessionId) {
657
+ if (!sessionId) return [];
658
+ var list = bookmarkMap()[sessionId];
659
+ return Array.isArray(list) ? list : [];
660
+ };
661
+ var writeBookmarks = function (sessionId, list) {
662
+ if (!sessionId) return;
663
+ try {
664
+ var map = bookmarkMap();
665
+ if (list && list.length) map[sessionId] = list;
666
+ else delete map[sessionId];
667
+ // trim the OLDEST sessions first, so a long-lived install cannot grow forever
668
+ var ids = Object.keys(map);
669
+ if (ids.length > BOOKMARK_MAX) {
670
+ ids.sort(function (a, b) {
671
+ var aa = (map[a][0] && map[a][0].at) || 0;
672
+ var bb = (map[b][0] && map[b][0].at) || 0;
673
+ return aa - bb;
674
+ });
675
+ for (var i = 0; i < ids.length - BOOKMARK_MAX; i++) delete map[ids[i]];
676
+ }
677
+ localStorage.setItem(BOOKMARK_KEY, JSON.stringify(map));
678
+ } catch (e) { /* storage blocked: bookmarks are a convenience */ }
679
+ };
464
680
  // ---- recent search history (browser-local, like the reading position) --
465
681
  // Most-recent-first [{q, scope}]: filled when a search is committed (Enter in
466
682
  // the box, a result click), offered back as one-tap chips while the box is
@@ -554,6 +770,7 @@ window.__ModuleLoader__.load({
554
770
  var clampRatio = function (raw, fallback) {
555
771
  return raw !== null && isFinite(Number(raw)) ? Math.min(1, Math.max(0, Math.round(Number(raw) * 100) / 100)) : fallback;
556
772
  };
773
+
557
774
  return {
558
775
  dock: dockRaw === "right" ? "right" : "left",
559
776
  // 0 means "the panel's own default" (see PANEL_WIDTH / auto height)
@@ -562,6 +779,11 @@ window.__ModuleLoader__.load({
562
779
  h: hRaw !== null && isFinite(Number(hRaw)) && Number(hRaw) >= 60 ? Number(hRaw) : 0,
563
780
  levels: parseLevels(rawLevels) || LEVELS_ALL.slice(),
564
781
  density: read(PREF_KEYS.density) === "compact" ? "compact" : "comfy",
782
+ accent: normAccent(read(PREF_KEYS.accent)),
783
+ // absent means "on": the translucent look is what this version ships
784
+ glass: read(PREF_KEYS.glass) !== "0",
785
+ glassStrength: clampPct(read(PREF_KEYS.glassStrength), 60),
786
+ sheetHeight: clampFrac(read(PREF_KEYS.sheetHeight), 1),
565
787
  zoom: clampRatio(zoomRaw, 1),
566
788
  sheetZoom: clampRatio(sheetZoomRaw, 1),
567
789
  handle: clampRatio(handleRaw, 0.5),
@@ -584,6 +806,10 @@ window.__ModuleLoader__.load({
584
806
  h: function (v) { return String(v); },
585
807
  levels: function (v) { return JSON.stringify(v); },
586
808
  density: function (v) { return v === "compact" ? "compact" : "comfy"; },
809
+ accent: function (v) { return normAccent(v); },
810
+ glass: function (v) { return v ? "1" : "0"; },
811
+ glassStrength: function (v) { return String(v); },
812
+ sheetHeight: function (v) { return String(v); },
587
813
  zoom: function (v) { return String(v); },
588
814
  sheetZoom: function (v) { return String(v); },
589
815
  handle: function (v) { return String(v); },
@@ -619,6 +845,10 @@ window.__ModuleLoader__.load({
619
845
  lang: v.lang === "zh" || v.lang === "en" ? v.lang : "auto",
620
846
  levels: levels || LEVELS_ALL.slice(),
621
847
  density: v.density === "compact" ? "compact" : "comfy",
848
+ accent: normAccent(v.accent),
849
+ glass: v.glass !== false,
850
+ glassStrength: clampPct(v.glassStrength, 60),
851
+ sheetHeight: clampFrac(v.sheetHeight, 1),
622
852
  zoom: isFinite(zoomNum) ? Math.min(2, Math.max(0.5, Math.round(zoomNum * 100) / 100)) : 1,
623
853
  sheetZoom: isFinite(sheetZoomNum) ? Math.min(2, Math.max(0.5, Math.round(sheetZoomNum * 100) / 100)) : 1,
624
854
  handle: isFinite(handleNum) ? Math.min(1, Math.max(0, Math.round(handleNum * 100) / 100)) : 0.5,
@@ -635,7 +865,9 @@ window.__ModuleLoader__.load({
635
865
  if (!over) return local;
636
866
  return {
637
867
  dock: over.dock, y: local.y, w: local.w, h: local.h,
638
- levels: over.levels, density: over.density, zoom: over.zoom, sheetZoom: over.sheetZoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
868
+ levels: over.levels, density: over.density, accent: over.accent,
869
+ glass: over.glass, glassStrength: over.glassStrength, sheetHeight: over.sheetHeight,
870
+ zoom: over.zoom, sheetZoom: over.sheetZoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
639
871
  lang: over.lang, remember: over.remember, debug: over.debug, autoLoad: over.autoLoad
640
872
  };
641
873
  };
@@ -1875,7 +2107,7 @@ window.__ModuleLoader__.load({
1875
2107
  // NOTE: these fills sit ON TOP OF conversation text, so they stay deliberately
1876
2108
  // stronger than the panel's UI tints (C.chip / C.groupTint); only the current
1877
2109
  // hit's outline reuses the shared accent value.
1878
- function highlightRow(row, q, currentOcc, fuzzy) {
2110
+ function highlightRow(row, q, currentOcc, fuzzy, rgb) {
1879
2111
  if (!row || !q) return;
1880
2112
  var fq = foldQuery(q);
1881
2113
  if (!fq) return;
@@ -1906,12 +2138,13 @@ window.__ModuleLoader__.load({
1906
2138
  if (from < cursor || to <= from) continue;
1907
2139
  if (from > cursor) frag.appendChild(document.createTextNode(text.slice(cursor, from)));
1908
2140
  var mark = document.createElement("span");
2141
+ var hitRgb = rgb || "79,140,255";
1909
2142
  if (isCurrent) {
1910
2143
  mark.className = "dqt-current";
1911
- mark.style.background = "rgba(79,140,255,0.55)";
1912
- mark.style.boxShadow = "0 0 0 1px rgba(79,140,255,0.85)";
2144
+ mark.style.background = "rgba(" + hitRgb + ",0.55)";
2145
+ mark.style.boxShadow = "0 0 0 1px rgba(" + hitRgb + ",0.85)";
1913
2146
  } else {
1914
- mark.style.background = "rgba(79,140,255,0.32)";
2147
+ mark.style.background = "rgba(" + hitRgb + ",0.32)";
1915
2148
  }
1916
2149
  mark.style.borderRadius = "2px";
1917
2150
  mark.style.color = "inherit";
@@ -1933,6 +2166,9 @@ window.__ModuleLoader__.load({
1933
2166
  text: "var(--dsw-alias-label-primary, #e8eaee)",
1934
2167
  muted: "var(--dsw-alias-label-secondary, #9aa0ab)",
1935
2168
  accent: "var(--dsw-alias-brand-primary, #4f8cff)",
2169
+ // the selection triplet, for callers that paint their own alphas (search
2170
+ // hit marks): same hue as every tint above, in one place
2171
+ accentRgb: "79,140,255",
1936
2172
  // terminal turn failures (a reply that never arrived) get the host's own
1937
2173
  // error colour, so the outline speaks the same language as the transcript
1938
2174
  error: "var(--dsw-alias-state-error-primary, #e5534b)",
@@ -1952,8 +2188,59 @@ window.__ModuleLoader__.load({
1952
2188
  // edge at the left and right, so the negative spreads keep the vertical seams
1953
2189
  // free of shadow while the bottom — rounded corners included — lifts off the
1954
2190
  // conversation below.
1955
- sheetShadow: "0 14px 22px -12px rgba(0, 0, 0, 0.35), 0 6px 12px -10px rgba(0, 0, 0, 0.25)"
2191
+ sheetShadow: "0 14px 22px -12px rgba(0, 0, 0, 0.35), 0 6px 12px -10px rgba(0, 0, 0, 0.25)",
2192
+ // ---- glass surface (the 0.10.0 visual upgrade) -------------------------
2193
+ // The panel and the curtain are the same colour at GLASS_PCT plus a small
2194
+ // backdrop blur; the two 1px inset lines are the "light catching the edge"
2195
+ // that makes a translucent surface read as a material instead of a tint.
2196
+ // A small radius leaves the conversation TEXT recognisable-but-smudged, and
2197
+ // that fights the panel's own rows ("blurred and clear at once"). A wider
2198
+ // radius turns it into colour and light — the only thing that reads as a
2199
+ // surface rather than as a second layer of type.
2200
+ glassBlur: "blur(22px) saturate(1.25)",
2201
+ sheetBlur: "blur(20px) saturate(1.2)"
1956
2202
  };
2203
+ // Blur is painted by a CHILD layer, never by the surface itself: a
2204
+ // backdrop-filter on an element that also lives inside a clip-path container
2205
+ // gets promoted to its own compositing layer, and in the host's page that
2206
+ // promotion put the whole panel underneath the conversation. A child layer
2207
+ // keeps the surface's own stacking exactly as it was.
2208
+ var GLASS_OK = (function () {
2209
+ try {
2210
+ return typeof CSS !== "undefined" && CSS.supports
2211
+ && (CSS.supports("backdrop-filter", "blur(12px)") || CSS.supports("-webkit-backdrop-filter", "blur(12px)"));
2212
+ } catch (e) { return false; }
2213
+ })();
2214
+ // Two densities: the docked panel is a surface you read in, so it stays
2215
+ // denser (70 %); the curtain is a transient sheet over the conversation and
2216
+ // the reader is meant to see the page move behind it, so it goes lighter
2217
+ // (54 %). Below ~50 % the labels start to fight the blurred text underneath.
2218
+ C.glassBg = GLASS_OK
2219
+ ? "color-mix(in srgb, var(--dsw-alias-bg-base, rgba(24, 28, 36, 0.96)) 80%, transparent)"
2220
+ : C.panelBg;
2221
+ C.sheetBg = GLASS_OK
2222
+ ? "color-mix(in srgb, var(--dsw-alias-bg-base, rgba(24, 28, 36, 0.96)) 46%, transparent)"
2223
+ : C.panelBg;
2224
+ // The two 1px rim lines: a bright catch on the top edge and a whisper on the
2225
+ // bottom one. Dark surfaces need much less white to read the same way.
2226
+ var rimInset = function () {
2227
+ return isDarkScheme()
2228
+ ? "inset 0 1px 0 rgba(255,255,255,0.18), inset 0 -1px 0 rgba(255,255,255,0.05)"
2229
+ : "inset 0 1px 0 rgba(255,255,255,0.85), inset 0 -1px 0 rgba(255,255,255,0.35)";
2230
+ };
2231
+ // The material itself, as a layer that sits behind a surface's content
2232
+ // (z-index -1 inside the host surface's own stacking context).
2233
+ // Why the blur has to live on a SIBLING (docked panel) or on a surface with no
2234
+ // opacity of its own (the curtain):
2235
+ // · an element with `opacity` below 1 becomes a *backdrop root*, so a
2236
+ // backdrop-filter INSIDE it only ever samples that element's own content —
2237
+ // the blur comes out of nothing (that is why the child-layer attempt looked
2238
+ // exactly like no blur at all);
2239
+ // · putting it on the panel itself, inside the clip wrapper, got the panel
2240
+ // promoted to its own compositing layer and pushed under the conversation.
2241
+ // A sibling sits outside both problems: it samples the page, and the panel's
2242
+ // own translucent fill is painted over it.
2243
+
1957
2244
 
1958
2245
  // ---- theme scheme detection (for the panel's 3D inner shadow) ----
1959
2246
  // Prefer a positive dark signal from the OS preference, then DSH's theme
@@ -2073,6 +2360,43 @@ window.__ModuleLoader__.load({
2073
2360
  // leave a stale handle behind.
2074
2361
  var tocHost = props.tocHost;
2075
2362
  var sessionId = props.sessionId;
2363
+ // ---- bookmarks -----------------------------------------------------------
2364
+ // Read once per session switch; every mutation writes straight through, so
2365
+ // there is no second source of truth to reconcile.
2366
+ var _sMarks = react.useState(function () { return bookmarksOf(props.sessionId); });
2367
+ var marks = _sMarks[0];
2368
+ var setMarks = _sMarks[1];
2369
+ react.useEffect(function () {
2370
+ setMarks(bookmarksOf(props.sessionId));
2371
+ }, [props.sessionId]);
2372
+ var markSet = (function () {
2373
+ var m = {};
2374
+ for (var i = 0; i < marks.length; i++) if (marks[i] && marks[i].key) m[marks[i].key + "#" + (marks[i].idx === undefined ? 0 : marks[i].idx)] = true;
2375
+ return m;
2376
+ })();
2377
+ var isMarked = function (key, idx) { return !!markSet[key + "#" + (idx === undefined ? 0 : idx)]; };
2378
+ var toggleMark = function (key, idx, title, level) {
2379
+ if (!key) return;
2380
+ var id = key + "#" + (idx === undefined ? 0 : idx);
2381
+ var next = [];
2382
+ var found = false;
2383
+ for (var i = 0; i < marks.length; i++) {
2384
+ var e = marks[i];
2385
+ if (!e) continue;
2386
+ if (e.key + "#" + (e.idx === undefined ? 0 : e.idx) === id) { found = true; continue; }
2387
+ next.push(e);
2388
+ }
2389
+ if (!found) next.push({ key: key, idx: idx === undefined ? 0 : idx, title: title || "", level: level || 1, at: Date.now() });
2390
+ setMarks(next);
2391
+ writeBookmarks(props.sessionId, next);
2392
+ };
2393
+ // The bookmark list reuses the outline renderer: entries become ordinary rows
2394
+ // (same look, same jump, same hover/end control), so "only bookmarks" needs no
2395
+ // second view — just a different array of groups to draw.
2396
+ var _sOnlyMarks = react.useState(false);
2397
+ var onlyMarks = _sOnlyMarks[0];
2398
+ var setOnlyMarks = _sOnlyMarks[1];
2399
+ var toggleOnlyMarks = function () { setOnlyMarks(!onlyMarks); };
2076
2400
  var hostSessions = function () {
2077
2401
  if (!tocHost || typeof tocHost.sessions !== "function") return null;
2078
2402
  try {
@@ -2638,9 +2962,28 @@ window.__ModuleLoader__.load({
2638
2962
  var menuClosing = _sMenuClose[0];
2639
2963
  var setMenuClosing = _sMenuClose[1];
2640
2964
  var menuCloseTimerRef = react.useRef(null);
2965
+ // "已复制": the two copy entries write to the clipboard and used to close the
2966
+ // menu with no other trace, so there was no way to tell a copy from a misfire
2967
+ // (or the two entries apart). The label confirms it for a beat before the fade.
2968
+ var _sCopied = react.useState(null);
2969
+ var copiedId = _sCopied[0];
2970
+ var setCopiedId = _sCopied[1];
2971
+ var copyTimerRef = react.useRef(null);
2972
+ // Show the confirmation, then let the menu close on its own short fade.
2973
+ var flashCopied = function (id) {
2974
+ if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
2975
+ setCopiedId(id);
2976
+ copyTimerRef.current = setTimeout(function () {
2977
+ copyTimerRef.current = null;
2978
+ setCopiedId(null);
2979
+ closeRowMenu();
2980
+ }, 420);
2981
+ };
2641
2982
  var openRowMenu = function (e, info, key, idx) {
2642
2983
  if (e && e.preventDefault) e.preventDefault();
2643
2984
  if (e && e.stopPropagation) e.stopPropagation();
2985
+ if (copyTimerRef.current) { clearTimeout(copyTimerRef.current); copyTimerRef.current = null; }
2986
+ setCopiedId(null);
2644
2987
  var x = e && typeof e.clientX === "number" ? e.clientX : 0;
2645
2988
  var y = e && typeof e.clientY === "number" ? e.clientY : 0;
2646
2989
  if (menuCloseTimerRef.current) { clearTimeout(menuCloseTimerRef.current); menuCloseTimerRef.current = null; }
@@ -2817,6 +3160,21 @@ window.__ModuleLoader__.load({
2817
3160
  var _sAside = react.useState(null);
2818
3161
  var panelAside = _sAside[0];
2819
3162
  var setPanelAside = _sAside[1];
3163
+ // The outer halo may only relax to its full bleed once a slide has LANDED. Riding
3164
+ // the same transition let the shadow reach past the panel's own (still clipped)
3165
+ // edge during an expansion, which reads as a frame appearing before the surface.
3166
+ // Declared up here with the other hooks so an early "nothing to show" return never
3167
+ // changes the hook count.
3168
+ var lastLineRef = react.useRef(null);
3169
+ var _sShadowSettled = react.useState(true);
3170
+ var shadowSettled = _sShadowSettled[0];
3171
+ var setShadowSettled = _sShadowSettled[1];
3172
+ var geoOpenNow = open && !panelAside;
3173
+ react.useEffect(function () {
3174
+ if (!geoOpenNow) { setShadowSettled(false); return undefined; }
3175
+ var t = setTimeout(function () { setShadowSettled(true); }, 640);
3176
+ return function () { clearTimeout(t); };
3177
+ }, [geoOpenNow]);
2820
3178
  // The curtain hangs from just BELOW the centre column's view tabs (对话 / 轨迹 /
2821
3179
  // 上下文), not from the top of the scroll container: when those tabs live inside
2822
3180
  // that container, viewport.top sits above them and the curtain would cover them.
@@ -3113,6 +3471,132 @@ window.__ModuleLoader__.load({
3113
3471
  // two comparisons disagree forever — the panel then republishes its default on
3114
3472
  // every pass and stomps a fresh change with the value it had just read back.
3115
3473
  var prefs = usePrefs();
3474
+ // ---- the day strip (see its call site) -----------------------------------
3475
+ // Declared up here with the other hooks: an early "nothing to show yet"
3476
+ // return must not skip them, or the first empty render and every real one
3477
+ // disagree on the hook count (React #310). It reads the list's scroll
3478
+ // position directly — the labels are plain DOM and offsetTop is all the
3479
+ // answer needs — so the handler only ever sets one string.
3480
+ var _sDayStrip = react.useState("");
3481
+ var dayStripText = _sDayStrip[0];
3482
+ var setDayStripText = _sDayStrip[1];
3483
+ react.useEffect(function () {
3484
+ var el = listRef.current;
3485
+ if (!el || typeof el.addEventListener !== "function") return undefined;
3486
+ var sync = function () {
3487
+ var top = el.scrollTop || 0;
3488
+ if (top < 2) { setDayStripText(""); return; }
3489
+ var labels = typeof el.querySelectorAll === "function"
3490
+ ? el.querySelectorAll("[data-dqt-grouplabel]") : [];
3491
+ var cur = null;
3492
+ for (var i = 0; i < labels.length; i++) {
3493
+ if ((labels[i].offsetTop || 0) - 6 <= top) cur = labels[i];
3494
+ else break;
3495
+ }
3496
+ setDayStripText(cur ? String(cur.getAttribute("data-dqt-day") || "") : "");
3497
+ };
3498
+ el.addEventListener("scroll", sync);
3499
+ sync();
3500
+ return function () { el.removeEventListener("scroll", sync); };
3501
+ }, []);
3502
+ // ---- the curtain's own height (dragged from its bottom edge) -------------
3503
+ // The panel's blur twin needs the panel's REAL height: the panel's own height
3504
+ // is often auto (a stored 0), and a twin with no height blurs nothing — that
3505
+ // is what made the first twin invisible.
3506
+ //
3507
+ // Measured through a ResizeObserver, NOT on every render: reading offsetHeight
3508
+ // in a layout effect forces a synchronous layout of the whole (often huge)
3509
+ // outline on every single render, which is exactly the kind of thing that
3510
+ // turns a scroll or a jump into visible jank.
3511
+ var _sBlurH = react.useState(0);
3512
+ var panelBlurH = _sBlurH[0];
3513
+ var setPanelBlurH = _sBlurH[1];
3514
+ react.useEffect(function () {
3515
+ var el = panelRootRef.current;
3516
+ if (!el) return undefined;
3517
+ var measure = function () {
3518
+ var h = el.offsetHeight ? Math.round(el.offsetHeight) : 0;
3519
+ setPanelBlurH(function (prev) { return prev === h ? prev : h; });
3520
+ };
3521
+ if (typeof ResizeObserver === "function") {
3522
+ var ro = new ResizeObserver(measure);
3523
+ ro.observe(el);
3524
+ measure();
3525
+ return function () { ro.disconnect(); };
3526
+ }
3527
+ measure();
3528
+ return undefined;
3529
+ }, [open, sheetOpen]);
3530
+ var _sAutoGlass = react.useState(autoGlassOff);
3531
+ var autoGlassState = _sAutoGlass[0];
3532
+ var setAutoGlassOff = _sAutoGlass[1];
3533
+ var _sSheetFrac = react.useState(null);
3534
+ var sheetDragFrac = _sSheetFrac[0];
3535
+ var setSheetDragFrac = _sSheetFrac[1];
3536
+ var sheetDragRef = react.useRef(null);
3537
+ var sheetResizeStart = function (e) {
3538
+ if (!e || e.button === 2) return;
3539
+ var h = (sheetRef.current && sheetRef.current.clientHeight) || window.innerHeight || 600;
3540
+ sheetDragRef.current = { y: e.clientY, frac: prefs.sheetHeight, h: h };
3541
+ setSheetDragFrac(prefs.sheetHeight);
3542
+ try { if (e.currentTarget && e.currentTarget.setPointerCapture) e.currentTarget.setPointerCapture(e.pointerId); } catch (err) {}
3543
+ };
3544
+ var sheetResizeMove = function (e) {
3545
+ var d = sheetDragRef.current;
3546
+ if (!d) return;
3547
+ // dragging DOWN grows the curtain: the free edge is its bottom
3548
+ setSheetDragFrac(clampFrac(d.frac + (e.clientY - d.y) / d.h, 1));
3549
+ };
3550
+ var sheetResizeEnd = function () {
3551
+ if (!sheetDragRef.current) return;
3552
+ sheetDragRef.current = null;
3553
+ if (sheetDragFrac !== null) prefsStore.set({ sheetHeight: sheetDragFrac });
3554
+ setSheetDragFrac(null);
3555
+ };
3556
+ // ---- slow-device probe (see AUTO_OFF_KEY) --------------------------------
3557
+ // Sampled once per curtain opening, and only while the material is on: the
3558
+ // first 700ms of animated curtain is the heaviest thing this plugin does, so
3559
+ // a p90 frame time over ~26ms there means the blur is costing more than the
3560
+ // look is worth on this machine.
3561
+ react.useEffect(function () {
3562
+ if (!sheetOpen || !prefs.glass || autoGlassOff) return undefined;
3563
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") return undefined;
3564
+ var frames = [];
3565
+ var last = 0;
3566
+ var stop = false;
3567
+ var tick = function (t) {
3568
+ if (stop) return;
3569
+ if (last) frames.push(t - last);
3570
+ last = t;
3571
+ if (frames.length < 40) window.requestAnimationFrame(tick);
3572
+ else finish();
3573
+ };
3574
+ var finish = function () {
3575
+ stop = true;
3576
+ if (!frames.length) return;
3577
+ var sorted = frames.slice().sort(function (a, b) { return a - b; });
3578
+ var p90 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.9))];
3579
+ if (p90 > 26) {
3580
+ autoGlassOff = true;
3581
+ try { localStorage.setItem(AUTO_OFF_KEY, "1"); } catch (e) {}
3582
+ setAutoGlassOff(true);
3583
+ }
3584
+ };
3585
+ var id = window.requestAnimationFrame(tick);
3586
+ var guard = setTimeout(finish, 1200);
3587
+ return function () { stop = true; clearTimeout(guard); try { window.cancelAnimationFrame(id); } catch (e) {} };
3588
+ }, [sheetOpen]);
3589
+ // The render's own palette: blue shares the module object itself (default
3590
+ // path: zero behaviour change), any other theme merges the same keys.
3591
+ // Every C.* use below this line — rows, chips, the accent bar, hit marks —
3592
+ // follows the preference without touching a single call site.
3593
+ var C = glassC(themeC(prefs.accent), {
3594
+ glass: prefs.glass && !autoGlassState,
3595
+ glassStrength: prefs.glassStrength
3596
+ });
3597
+ // the once-injected stylesheet cannot see per-render values: repaint its
3598
+ // three theme-dependent rules whenever the theme colour changes
3599
+ react.useEffect(function () { applyAccentCss(prefs.accent); }, [prefs.accent]);
3116
3600
  // Resolve the language for this render FIRST: the group builder below bakes the
3117
3601
  // localised turn stamps (昨天 versus a numeric date) into its memo, and `langKey`
3118
3602
  // is what tells that memo a language switch has to rebuild it.
@@ -4952,18 +5436,33 @@ window.__ModuleLoader__.load({
4952
5436
  // (The collapsed panel is parked deliberately outside the area and is hidden
4953
5437
  // by its own clip, so only the open position is clamped.)
4954
5438
  var clampLeft = function (x) {
4955
- return Math.max(8, Math.min(Math.max(8, window.innerWidth - panelW - 8), x));
5439
+ // clamp against the width the panel will actually be drawn at (geoW, below):
5440
+ // the stored preference can exceed the window, and clamping to THAT put the
5441
+ // open edge in the wrong place
5442
+ return Math.max(8, Math.min(Math.max(8, window.innerWidth - geoW - 8), x));
4956
5443
  };
4957
5444
  // While the panel is being handed over to (or back from) the curtain it is
4958
5445
  // rendered at its COLLAPSED geometry, so the same transform/clip transition that
4959
5446
  // serves open/collapse also serves the handoff: no teleport, no flash.
4960
5447
  var geoOpen = open && !panelAside;
4961
- var anchorLeft = viewport ? viewport.left - panelW - 8 : -(panelW + 48);
5448
+ // The panel is docked AT the area's edge. When the stored width cannot fit beside
5449
+ // it, the WIDTH gives way for this render — never the position. Moving the open
5450
+ // panel inwards (the old fallback, via clampLeft) both covered the sidebar and
5451
+ // shortened the collapse below the panel's own width, so the tuck line stopped
5452
+ // agreeing with the line the panel came back out of: the panel appeared to close
5453
+ // to one place and reopen from another. The stored preference is untouched, so a
5454
+ // wider window brings the chosen width back.
5455
+ var geoW = Math.max(120, Math.min(panelW,
5456
+ dockRight
5457
+ ? (window.innerWidth - (viewport ? viewport.right + 48 : 60)) - 8
5458
+ : window.innerWidth - (viewport ? viewport.left : 0) - 16));
5459
+ // Parked against the area, one panel width further out.
5460
+ var anchorLeft = viewport ? viewport.left - geoW - 8 : -(geoW + 48);
4962
5461
  var openLeft;
4963
5462
  if (dockRight) {
4964
5463
  // RIGHT dock: keep the panel's VISIBLE right edge pinned at the SAME
4965
5464
  // line when collapsed as when expanded (the expand edge is correct).
4966
- var openRight = window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW;
5465
+ var openRight = window.innerWidth - (viewport ? viewport.right + 48 : 60) - geoW;
4967
5466
  openLeft = clampLeft(openRight);
4968
5467
  } else {
4969
5468
  // LEFT dock open (left dock is the confirmed-correct reference — untouched)
@@ -4986,15 +5485,35 @@ window.__ModuleLoader__.load({
4986
5485
  geoOpenRef.current = geoOpen;
4987
5486
  geoHoldRef.current = geoOpen ? 0 : Date.now() + COLLAPSE_HOLD_MS;
4988
5487
  geoSnapRef.current = geoOpen ? null : { anchorLeft: anchorLeft, openLeft: openLeft, dock: dock };
5488
+ // remember the line it is going into, so the next expansion can come back out
5489
+ // of exactly that line (see below)
5490
+ if (!geoOpen) lastLineRef.current = { openLeft: openLeft, dock: dock, w: window.innerWidth, h: window.innerHeight };
4989
5491
  }
4990
5492
  var geoFrozen = geoSnapRef.current !== null && geoSnapRef.current.dock === dock
4991
5493
  && Date.now() < geoHoldRef.current;
4992
5494
  if (geoFrozen) {
4993
5495
  anchorLeft = geoSnapRef.current.anchorLeft;
4994
5496
  openLeft = geoSnapRef.current.openLeft;
5497
+ } else {
5498
+ // The host re-measures its area between the two legs (a slow beat), and a few
5499
+ // pixels of drift there is what made the panel reopen slightly to the RIGHT of
5500
+ // the place it had tucked into. While the drift is small the line it went into
5501
+ // wins — over BOTH legs, so the tuck line and the open line are the same
5502
+ // number. A larger difference means the reader really moved something (the
5503
+ // sidebar, the window), and the fresh measurement wins instead.
5504
+ var lastLine = lastLineRef.current;
5505
+ if (lastLine && lastLine.dock === dock
5506
+ && lastLine.w === window.innerWidth && lastLine.h === window.innerHeight
5507
+ && Math.abs(lastLine.openLeft - openLeft) <= 24
5508
+ && lastLine.openLeft !== openLeft) {
5509
+ openLeft = lastLine.openLeft;
5510
+ // travel becomes geoW + 16 again, so `anchorLeft + clip` (the tuck line)
5511
+ // evaluates to this same line
5512
+ anchorLeft = openLeft - geoW - 16;
5513
+ }
4995
5514
  }
4996
5515
  var openOffset = openLeft - anchorLeft;
4997
- var parkedOffset = dockRight ? (openLeft + panelW) - anchorLeft : 0;
5516
+ var parkedOffset = dockRight ? (openLeft + geoW) - anchorLeft : 0;
4998
5517
  var panelLeft = anchorLeft;
4999
5518
  var panelOffset = geoOpen ? openOffset : parkedOffset;
5000
5519
  var panelTransform = panelOffset === 0 ? "none" : "translateX(" + zLen(panelOffset) + ")";
@@ -5091,11 +5610,23 @@ window.__ModuleLoader__.load({
5091
5610
  landed = true;
5092
5611
  if (glide === mine) glide = null;
5093
5612
  var ticks = 0;
5613
+ var lastSign = 0;
5094
5614
  var iv = setInterval(function () {
5095
- if (mine.done || (ticks += 1) > 5) { clearInterval(iv); return; }
5615
+ if (mine.done || (ticks += 1) > 3) { clearInterval(iv); return; }
5096
5616
  var next = getTarget(sp);
5097
- if (Math.abs(next - sp.scrollTop) > 2) sp.scrollTop = next;
5098
- }, 120);
5617
+ var delta = next - sp.scrollTop;
5618
+ // Only ever nudge a SMALL drift, and never twice in a row in opposite
5619
+ // directions. The re-check exists to absorb the last few pixels while
5620
+ // the host finishes measuring; if the measurement itself swings (a
5621
+ // virtualized turn settling under the target) an unbounded correction
5622
+ // yanks the reader away from where they just landed and then back —
5623
+ // "it arrives and flashes back". A jump that really moved is a fresh
5624
+ // glide, not this one's business.
5625
+ if (Math.abs(delta) > 60) { clearInterval(iv); return; }
5626
+ var sign = delta > 0 ? 1 : (delta < 0 ? -1 : 0);
5627
+ if (sign !== 0 && lastSign !== 0 && sign !== lastSign) { clearInterval(iv); return; }
5628
+ if (Math.abs(delta) > 8) { sp.scrollTop = next; lastSign = sign; }
5629
+ }, 140);
5099
5630
  };
5100
5631
 
5101
5632
  // fallback: draw the glide ourselves, in real time, decelerating to a stop
@@ -5289,7 +5820,7 @@ window.__ModuleLoader__.load({
5289
5820
  // a jump that started from a search hit keeps the hit highlighted
5290
5821
  if (query.trim()) {
5291
5822
  clearHighlights();
5292
- highlightRow(row, query.trim(), 0, fuzzy);
5823
+ highlightRow(row, query.trim(), 0, fuzzy, C.accentRgb);
5293
5824
  }
5294
5825
  return;
5295
5826
  }
@@ -5481,7 +6012,7 @@ window.__ModuleLoader__.load({
5481
6012
  // occurrence index within the target message (consecutive in matches)
5482
6013
  var occ = 0;
5483
6014
  for (var p = i - 1; p >= 0 && matches[p].key === m.key; p--) occ++;
5484
- highlightRow(r, q, occ, fuzzy);
6015
+ highlightRow(r, q, occ, fuzzy, C.accentRgb);
5485
6016
  var markEl = r.querySelector(".dqt-current");
5486
6017
  var sp = r.closest ? r.closest("[data-conversation-scroll]") : null;
5487
6018
  if (sp && markEl) {
@@ -5540,7 +6071,7 @@ window.__ModuleLoader__.load({
5540
6071
  title: tip,
5541
6072
  onMouseEnter: function (e) {
5542
6073
  e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
5543
- e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
6074
+ e.currentTarget.style.color = C.accent;
5544
6075
  },
5545
6076
  onMouseLeave: function (e) {
5546
6077
  e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
@@ -5565,6 +6096,10 @@ window.__ModuleLoader__.load({
5565
6096
  onOpenTurn: openTurn,
5566
6097
  hoverStart: hoverStart,
5567
6098
  hoverEnd: hoverEnd,
6099
+ // B: which rows carry a bookmark (the flag layer reads this)
6100
+ isMarked: isMarked,
6101
+ // B: the ribbon's colour, resolved here to a concrete hex (see renderItem)
6102
+ markColor: (ACCENTS[normAccent(prefs.accent)] || ACCENTS.blue).dot,
5568
6103
  // right-click on a row: pin its preview / jump to the end of its section
5569
6104
  rowMenu: openRowMenu,
5570
6105
  // the curtain is wide: rows switch to a one-line, multi-part layout that
@@ -5787,12 +6322,26 @@ window.__ModuleLoader__.load({
5787
6322
  // the wipe edge drift 24px through the slide); the shadow lives on the panel's
5788
6323
  // twin element instead.
5789
6324
  var panelClip = "inset(0 0 0 0px)";
6325
+ // ONE line, and that is a geometric necessity rather than a taste: the visible
6326
+ // edge of a clipped box can never be left of the box's own left edge, so the
6327
+ // only way the tuck line and the open line can both sit somewhere is for the
6328
+ // wipe to equal the travel. Any other value makes the edge sweep between two
6329
+ // lines through the animation (pull it 8px back and the panel visibly slides
6330
+ // 8px further left as it vanishes, then reappears from the open line instead).
6331
+ //
6332
+ // It can never be SHORTER than the box it hides either: `clampLeft` pulls the
6333
+ // open position inwards when the window is narrow relative to the panel, which
6334
+ // shortened the travel below the panel width and left a strip of the parked
6335
+ // panel standing on the workspace (measured at 284px on a 13" screen). (The
6336
+ // right dock needs no guard — its travel is one panel width by construction.)
6337
+ // The halo twin reads the same number, so the two are clipped by one wipe.
6338
+ var clipWipe = Math.max(travelPx, geoW + 2);
5790
6339
  if (!geoOpen) {
5791
6340
  if (dockRight) {
5792
6341
  // mirror of LEFT: clip the RIGHT side (panel flies right, shrinking from its right)
5793
6342
  panelClip = "inset(0 " + zLen(travelPx) + " 0 0)";
5794
6343
  } else {
5795
- panelClip = "inset(0 0 0 " + zLen(travelPx) + ")";
6344
+ panelClip = "inset(0 0 0 " + zLen(clipWipe) + ")";
5796
6345
  }
5797
6346
  }
5798
6347
  // faded out entirely while another center-column view is active
@@ -5807,7 +6356,11 @@ window.__ModuleLoader__.load({
5807
6356
  // ramp over the slide only made the two directions read different lines.
5808
6357
  // While parked the wipe covers the box entirely, so the resting opacity can
5809
6358
  // simply stay up.
5810
- var panelOpacity = chatViewActive ? (hovered ? 1 : 0.72) : 0;
6359
+ // Resting opacity used to be 0.72 so the panel stepped out of the way. The
6360
+ // surface is genuinely translucent now (74 % + blur), and dimming on top of
6361
+ // that would leave the labels at ~53 % — text has to stay crisp, so the
6362
+ // "inactive" hint is a much smaller step.
6363
+ var panelOpacity = chatViewActive ? (hovered ? 1 : 0.92) : 0;
5811
6364
 
5812
6365
  // ---- toolbar buttons -----------------------------------------------------
5813
6366
  // ONE instance of each, placed either in the docked header row or in the
@@ -5845,7 +6398,7 @@ window.__ModuleLoader__.load({
5845
6398
  onMouseEnter: function (e) {
5846
6399
  if (disabled) return;
5847
6400
  e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
5848
- e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
6401
+ e.currentTarget.style.color = C.accent;
5849
6402
  },
5850
6403
  onMouseLeave: function (e) {
5851
6404
  e.currentTarget.style.background = active ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
@@ -5963,6 +6516,53 @@ window.__ModuleLoader__.load({
5963
6516
  // is what makes it read centred (the old character needed the same, plus a y nudge for
5964
6517
  // its line box, which a path does not have)
5965
6518
  var dockBtnEl = iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockTriEl, 12, dockRight ? { x: -1, y: 0 } : { x: 1, y: 0 });
6519
+ // B: the bookmark button. Always present, between the two left-hand controls,
6520
+ // and it is the way in and out of the bookmark view — a control that appeared
6521
+ // and vanished with the count left the reader looking for it.
6522
+ //
6523
+ // The star is the whole face: no number inside it (there is no room in a
6524
+ // twenty-pixel star, and a squeezed digit is worse than none). The count lives
6525
+ // in the tooltip a moment later, where it has a line of its own.
6526
+ var markCount = marks.length > 99 ? "99+" : String(marks.length);
6527
+ var marksTip = onlyMarks
6528
+ ? Tp("marks.tip.all", { n: markCount })
6529
+ : Tp("marks.tip.only", { n: markCount });
6530
+ var marksBtnEl = react_jsx_runtime.jsx("button", {
6531
+ onClick: toggleOnlyMarks,
6532
+ title: marksTip,
6533
+ "aria-label": marksTip,
6534
+ "data-dqt-marks": onlyMarks ? "only" : "all",
6535
+ style: {
6536
+ width: 24,
6537
+ height: 24,
6538
+ display: "flex",
6539
+ alignItems: "center",
6540
+ justifyContent: "center",
6541
+ borderRadius: "50%",
6542
+ cornerShape: "round",
6543
+ background: onlyMarks ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
6544
+ border: "none",
6545
+ color: C.muted,
6546
+ cursor: "pointer",
6547
+ flex: "none",
6548
+ padding: 0,
6549
+ transition: "background 0.18s ease, color 0.18s ease"
6550
+ },
6551
+ children: react_jsx_runtime.jsx("svg", {
6552
+ key: "star",
6553
+ width: 15, height: 15, viewBox: "0 0 24 24",
6554
+ style: { display: "block" },
6555
+ children: react_jsx_runtime.jsx("path", {
6556
+ // rounded joins (a stroke in the same colour as the fill) soften the
6557
+ // five points; without it the star reads as a spiky asterisk at this size
6558
+ d: "M12 3.1l2.72 5.52 6.09.89-4.4 4.29 1.04 6.06L12 16.9l-5.45 2.86 1.04-6.06-4.4-4.29 6.09-.89z",
6559
+ fill: "currentColor",
6560
+ stroke: "currentColor",
6561
+ strokeWidth: 2.4,
6562
+ strokeLinejoin: "round"
6563
+ })
6564
+ })
6565
+ }, "marks-btn");
5966
6566
  // the curtain keeps its own entry point: the water-drop handle on the top
5967
6567
  // edge (sheet.openHandle). The toolbar's curtain button is gone — the header
5968
6568
  // is four controls now.
@@ -6063,7 +6663,7 @@ window.__ModuleLoader__.load({
6063
6663
  // handle.
6064
6664
  react_jsx_runtime.jsx("div", {
6065
6665
  style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
6066
- children: [settingsBtnEl, dockBtnEl]
6666
+ children: [settingsBtnEl, marksBtnEl, dockBtnEl]
6067
6667
  }),
6068
6668
  // spacer: nothing here, it just pins the two pairs to the two ends —
6069
6669
  // and it is the first thing to give way when the panel narrows
@@ -6318,7 +6918,7 @@ window.__ModuleLoader__.load({
6318
6918
  borderRadius: "50%",
6319
6919
  cornerShape: "round",
6320
6920
  fontSize: sheetOpen ? 13 : 11,
6321
- color: searchScope !== "title" ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
6921
+ color: searchScope !== "title" ? C.accent : C.muted,
6322
6922
  background: searchScope !== "title" ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
6323
6923
  border: "none",
6324
6924
  cursor: "pointer",
@@ -6360,7 +6960,7 @@ window.__ModuleLoader__.load({
6360
6960
  borderRadius: "50%",
6361
6961
  cornerShape: "round",
6362
6962
  fontSize: sheetOpen ? 13 : 11,
6363
- color: fuzzy ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
6963
+ color: fuzzy ? C.accent : C.muted,
6364
6964
  background: fuzzy ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
6365
6965
  border: "none",
6366
6966
  cursor: searchScope === "cross" ? "default" : "pointer",
@@ -6405,14 +7005,136 @@ window.__ModuleLoader__.load({
6405
7005
  },
6406
7006
  children: hint === "oldest" ? T("search.hintOldest") : T("search.hintMore")
6407
7007
  }) : null;
7008
+ // B: "only bookmarks". The rows come from the stored entries rather than from
7009
+ // the outline tree — a bookmark may point at a turn that is not loaded right
7010
+ // now, and it still has to be listed (it is a place the reader saved).
7011
+ //
7012
+ // These are DELIBERATELY plain rows, not the outline's own row renderer. That
7013
+ // renderer registers data-jump-key / data-nav-row and wires the hover card,
7014
+ // the end control and the preview pinning; reusing it here put a second set of
7015
+ // rows into the DOM carrying the SAME keys as the live outline, which is a
7016
+ // collision waiting to happen for anything that queries rows by key (the jump
7017
+ // target lookup, the keyboard cursor, the follow). A saved place needs a label
7018
+ // and a click — nothing else.
7019
+ var markRowsEl = null;
7020
+ if (onlyMarks && !resultsEl) {
7021
+ // `dense` is a local of the row renderers, not of this component: read the
7022
+ // preference here instead (referencing the other one threw and took the
7023
+ // whole panel down with it).
7024
+ var markDense = prefs.density === "compact";
7025
+ markRowsEl = marks.length ? react_jsx_runtime.jsx("div", {
7026
+ key: "marks",
7027
+ style: { padding: "2px 0 8px" },
7028
+ children: marks.map(function (e, i) {
7029
+ var lvl = Math.min(6, Math.max(1, e.level || 1));
7030
+ return react_jsx_runtime.jsx("div", {
7031
+ "data-dqt-markrow": "1",
7032
+ onClick: function () { jump(e.key, e.idx); },
7033
+ title: e.title || "",
7034
+ style: {
7035
+ display: "flex", alignItems: "center", gap: 6,
7036
+ padding: markDense ? "2px 8px 2px 7px" : "4px 8px 4px 7px",
7037
+ borderRadius: 6, cursor: "pointer",
7038
+ fontSize: 12.5, color: C.text,
7039
+ transition: "background-color 0.15s ease"
7040
+ },
7041
+ onMouseEnter: function (ev) { ev.currentTarget.style.backgroundColor = C.hover; },
7042
+ onMouseLeave: function (ev) { ev.currentTarget.style.backgroundColor = "transparent"; },
7043
+ children: [
7044
+ react_jsx_runtime.jsx("span", {
7045
+ key: "f", style: { color: C.accent, display: "flex", flex: "none" },
7046
+ children: react_jsx_runtime.jsx("svg", {
7047
+ width: 7, height: 9, viewBox: "0 0 7 9", fill: "currentColor",
7048
+ style: { display: "block" },
7049
+ children: react_jsx_runtime.jsx("path", { d: "M0 0h5.6L4.1 2.2l1.5 2.2H1.1V9H0z" })
7050
+ })
7051
+ }),
7052
+ react_jsx_runtime.jsx("span", {
7053
+ key: "l", style: { flex: "none", fontSize: 10, color: C.muted, width: 16 },
7054
+ children: "H" + lvl
7055
+ }),
7056
+ react_jsx_runtime.jsx("span", {
7057
+ key: "t", style: { flex: "1 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
7058
+ children: e.title || ""
7059
+ })
7060
+ ]
7061
+ }, "mark-row-" + i);
7062
+ })
7063
+ }) : react_jsx_runtime.jsx("div", {
7064
+ key: "marks-empty",
7065
+ style: {
7066
+ display: "flex", alignItems: "center", justifyContent: "center",
7067
+ padding: "30px 16px", color: C.muted, fontSize: 11.5, lineHeight: "17px",
7068
+ textAlign: "center"
7069
+ },
7070
+ children: T("marks.empty")
7071
+ });
7072
+ }
6408
7073
  var outlineEl = renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup,
6409
7074
  questionsOnly ? Object.assign({}, renderExtra, { questionsOnly: true }) : renderExtra);
7075
+ // M: an empty outline used to be a blank box, which reads as "this is broken"
7076
+ // rather than "there is nothing here yet". One quiet line plus a faded glyph
7077
+ // says which it is. A turn with no headings still becomes a group, so this
7078
+ // only appears for a conversation that has nothing to outline AT ALL.
7079
+ var emptyEl = !resultsEl && shownGroups.length === 0 ? react_jsx_runtime.jsx("div", {
7080
+ key: "empty",
7081
+ style: {
7082
+ display: "flex", flexDirection: "column", alignItems: "center",
7083
+ justifyContent: "center", gap: 8, padding: "34px 18px",
7084
+ color: C.muted, textAlign: "center"
7085
+ },
7086
+ children: [
7087
+ react_jsx_runtime.jsx("svg", {
7088
+ key: "e-ico", width: 26, height: 26, viewBox: "0 0 24 24", fill: "none",
7089
+ stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round",
7090
+ style: { opacity: 0.5, display: "block" },
7091
+ children: [
7092
+ react_jsx_runtime.jsx("line", { key: "a", x1: 4, y1: 6, x2: 20, y2: 6 }),
7093
+ react_jsx_runtime.jsx("line", { key: "b", x1: 4, y1: 12, x2: 15, y2: 12 }),
7094
+ react_jsx_runtime.jsx("line", { key: "c", x1: 4, y1: 18, x2: 11, y2: 18 })
7095
+ ]
7096
+ }),
7097
+ react_jsx_runtime.jsx("div", {
7098
+ key: "e-txt",
7099
+ style: { fontSize: 11.5, lineHeight: "17px", opacity: 0.85, maxWidth: 210 },
7100
+ children: T("outline.empty")
7101
+ })
7102
+ ]
7103
+ }) : null;
6410
7104
  // The curtain keeps the outline in the middle of the surface (its own column,
6411
7105
  // centred with room to spare at both sides) and pushes the search in from the
6412
7106
  // right; the docked panel keeps its original "results replace the list".
7107
+ // The bookmark view rides ON TOP of the outline instead of replacing it, and
7108
+ // the outline is only hidden (visibility, not display): it keeps its height,
7109
+ // so the scroller keeps its offset and closing the view lands the reader
7110
+ // exactly where they were. Replacing the content clamped the scroll to the top
7111
+ // the moment the shorter list rendered, and putting the offset back afterwards
7112
+ // was a race the browser usually won.
7113
+ var marksOverlayEl = null;
7114
+ if (onlyMarks && !resultsEl) {
7115
+ marksOverlayEl = react_jsx_runtime.jsx("div", {
7116
+ className: "dqt-marks-overlay",
7117
+ style: {
7118
+ position: "absolute",
7119
+ left: 0,
7120
+ // the curtain's search column keeps its own strip: the overlay covers the
7121
+ // outline, not the hits
7122
+ right: sheetOpen && sheetColW > 0 ? sheetColW : 0,
7123
+ top: 0,
7124
+ bottom: 0,
7125
+ overflowY: "auto",
7126
+ overflowAnchor: "none",
7127
+ padding: sheetOpen ? "0 20px 8px" : "0 8px 6px",
7128
+ direction: (sheetOpen || dockRight) ? "ltr" : "rtl"
7129
+ },
7130
+ children: react_jsx_runtime.jsx("div", { style: { direction: "ltr" }, children: markRowsEl })
7131
+ });
7132
+ }
7133
+ // across the curtain the results live in their own column, so the outline stays
7134
+ // where it is; only the docked panel swaps the list for the hits
6413
7135
  var listChildren = sheetOpen
6414
- ? [outlineEl, hintEl]
6415
- : (resultsEl ? [resultsEl, hintEl] : outlineEl);
7136
+ ? [outlineEl || emptyEl, hintEl]
7137
+ : (resultsEl ? [resultsEl, hintEl] : (outlineEl || emptyEl));
6416
7138
  var listEl = react_jsx_runtime.jsx("div", {
6417
7139
  ref: listRef,
6418
7140
  className: "dqt-list",
@@ -6422,6 +7144,9 @@ window.__ModuleLoader__.load({
6422
7144
  role: "group",
6423
7145
  "aria-label": T("a11y.list"),
6424
7146
  style: {
7147
+ // hidden rather than unmounted while the bookmark view is up (see above):
7148
+ // it keeps its height and therefore its scroll offset
7149
+ visibility: onlyMarks && !resultsEl ? "hidden" : undefined,
6425
7150
  overflowY: "auto",
6426
7151
  // We are the only thing that holds the reader's place in this list (see the
6427
7152
  // paging block): the browser's own scroll anchoring would also adjust
@@ -6434,6 +7159,11 @@ window.__ModuleLoader__.load({
6434
7159
  // header that many pixels below the toolbar (a visible gap). Spacing
6435
7160
  // between groups comes from the per-group divider instead.
6436
7161
  padding: sheetOpen ? "0 20px 8px" : "0 8px 6px",
7162
+ // the rows dissolve at BOTH edges instead of being cut by a hard line. The
7163
+ // top fade is safe now that the day strip lives outside the scroller: it
7164
+ // thins the rows approaching the strip, never the label itself.
7165
+ maskImage: "linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 22px), transparent 100%)",
7166
+ WebkitMaskImage: "linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 22px), transparent 100%)",
6437
7167
  flex: "1 1 auto",
6438
7168
  minHeight: 0,
6439
7169
  // across the curtain the list does not touch the edges: it is a centred
@@ -6499,10 +7229,36 @@ window.__ModuleLoader__.load({
6499
7229
  var showBack = awayFromRestore(listRef.current);
6500
7230
  // the outline and the search column share one row in BOTH layouts, so the list
6501
7231
  // is never moved between parents when the curtain opens or closes
7232
+ var dayStripEl = react_jsx_runtime.jsx("div", {
7233
+ "data-dqt-daystrip": "1",
7234
+ style: {
7235
+ flex: "none",
7236
+ // the space is reserved even while empty, so the list never jumps when
7237
+ // the first label scrolls under the edge. A STRING on purpose: the group
7238
+ // header's own height is the number 18, and several checks use that to
7239
+ // find header rows — the strip must not be mistaken for one.
7240
+ height: "18px",
7241
+ display: "flex",
7242
+ alignItems: "center",
7243
+ padding: sheetOpen ? "0 20px" : "0 10px",
7244
+ fontSize: 11,
7245
+ lineHeight: "14px",
7246
+ color: C.muted,
7247
+ whiteSpace: "nowrap",
7248
+ overflow: "hidden",
7249
+ textOverflow: "ellipsis",
7250
+ pointerEvents: "none"
7251
+ },
7252
+ // the bookmark view is a different list with its own scroll: the day label
7253
+ // would go on showing the last day the outline was scrolled to
7254
+ children: onlyMarks ? "" : dayStripText
7255
+ });
6502
7256
  var bodyRowEl = react_jsx_runtime.jsx("div", {
6503
7257
  className: "dqt-body-row",
6504
- style: { flex: "1 1 auto", minHeight: 0, display: "flex", flexDirection: "row", alignItems: "stretch" },
6505
- children: [listEl, sheetSearchColEl]
7258
+ // `position: relative` is what lets the bookmark overlay cover the list
7259
+ // without becoming part of its scrolled content
7260
+ style: { position: "relative", flex: "1 1 auto", minHeight: 0, display: "flex", flexDirection: "row", alignItems: "stretch" },
7261
+ children: [listEl, marksOverlayEl, sheetSearchColEl]
6506
7262
  });
6507
7263
  // one geometry for the panel and its shadow twin, merged (never duplicated)
6508
7264
  var mergeStyle = function (base, extra) {
@@ -6516,11 +7272,11 @@ window.__ModuleLoader__.load({
6516
7272
  position: "fixed",
6517
7273
  top: zLen(baseTopPx),
6518
7274
  left: zLen(panelLeft),
6519
- width: zLen(panelW),
7275
+ width: zLen(geoW),
6520
7276
  maxHeight: zLen(vMaxH),
6521
7277
  height: panelH > 0 ? zLen(panelH) : undefined,
6522
7278
  zoom: zoom === 1 ? undefined : zoom,
6523
- borderRadius: 12,
7279
+ borderRadius: RADIUS_PANEL,
6524
7280
  transform: panelTransform,
6525
7281
  transition: viewFading ? "opacity 0.3s ease" : panelTransition,
6526
7282
  // held back while the edge handle slips home (see edgeLeaving): the panel is
@@ -6542,20 +7298,80 @@ window.__ModuleLoader__.load({
6542
7298
  // One halo on ALL four sides — evenly, corners included (a card lifted off
6543
7299
  // the conversation; no downward bias, or the top corners stay bare). Dark
6544
7300
  // scheme gets a WHITE one — the theme's own raised language.
6545
- // the wipe side's cut sits exactly on the seam the panel tucks behind: past
6546
- // that line there is nothing to see — panel or halo — the clip decides,
6547
- // never a fade
7301
+ //
7302
+ // SHADOW_REACH: how far the edge OPPOSITE the wipe is pulled in while the panel
7303
+ // is parked, so the twin's halo cannot spill past the tuck line once the panel
7304
+ // body has been clipped away.
7305
+ // SHADOW_BLEED: how far all four sides bleed while the panel is at rest — the
7306
+ // deepest reach of the halo (15px below with the offset, more at the corners).
6548
7307
  var SHADOW_REACH = 8;
6549
7308
  var SHADOW_BLEED = 24;
6550
- var shadowWipe = geoOpen ? 0 : travelPx;
7309
+ // The WIPE side is the one the panel slides through. It is clipped by exactly the
7310
+ // same wipe as the panel while anything is moving — zero reach, so the halo is
7311
+ // never drawn where the panel itself is still hidden (any reach there reads as an
7312
+ // outer frame arriving before the surface). The halo the reader wants lives on the
7313
+ // OTHER three sides during the slide, and on this side only once the slide has
7314
+ // LANDED, when it relaxes to the full bleed.
7315
+ //
7316
+ // The relaxation waits for `shadowSettled` (set by the timer in the hook block
7317
+ // above) instead of riding the same transition: transitioning it together was
7318
+ // what let the halo lead the panel's clipped edge.
7319
+ var shadowDockSide = (geoOpen && shadowSettled)
7320
+ ? -SHADOW_BLEED
7321
+ : (geoOpen ? 0 : (dockRight ? travelPx : clipWipe));
6551
7322
  // Keep the four-sided halo while open. During a tuck, move the wipe edge
6552
7323
  // with the panel AND bring the opposite horizontal clip edge inward to meet
6553
7324
  // it at the parked position. Leaving that edge at -24px let the shadow twin
6554
7325
  // paint a visible strip after the panel body had been clipped away (wider on
6555
7326
  // the right dock, a thin sliver on the left dock).
6556
7327
  var shadowClip = dockRight
6557
- ? "inset(" + zLen(-SHADOW_BLEED) + " " + zLen(shadowWipe - SHADOW_REACH) + " " + zLen(-SHADOW_BLEED) + " " + zLen(geoOpen ? -SHADOW_BLEED : SHADOW_REACH) + ")"
6558
- : "inset(" + zLen(-SHADOW_BLEED) + " " + zLen(geoOpen ? -SHADOW_BLEED : -SHADOW_REACH) + " " + zLen(-SHADOW_BLEED) + " " + zLen(shadowWipe - SHADOW_REACH) + ")";
7328
+ ? "inset(" + zLen(-SHADOW_BLEED) + " " + zLen(shadowDockSide) + " " + zLen(-SHADOW_BLEED) + " " + zLen(geoOpen ? -SHADOW_BLEED : SHADOW_REACH) + ")"
7329
+ : "inset(" + zLen(-SHADOW_BLEED) + " " + zLen(geoOpen ? -SHADOW_BLEED : -SHADOW_REACH) + " " + zLen(-SHADOW_BLEED) + " " + zLen(shadowDockSide) + ")";
7330
+ // The panel's blur lives OUTSIDE the clipping wrapper (see the portal list):
7331
+ // inside it, any backdrop-filter promotes a layer in that subtree and the
7332
+ // panel drops beneath the conversation. As a body-level twin with the same
7333
+ // geometry it samples the page normally, and the panel's own translucent
7334
+ // fill is painted over it.
7335
+ // A body-level blur twin was tried and removed: it composites as a SECOND
7336
+ // pass, so the panel's own translucency showed the page straight through
7337
+ // while the twin showed it blurred — the reader sees "one blurred layer and
7338
+ // one clear layer" stacked. A surface can only read as ONE material when the
7339
+ // blur and the fill are painted by the same element, and that is impossible
7340
+ // for the docked panel where it currently lives (a backdrop-filter anywhere
7341
+ // inside its clipping wrapper drops the panel under the conversation).
7342
+ // The blur twin. It lives as a DIRECT child of the clipping shell — the same
7343
+ // depth as the curtain, which is the only place in this subtree where a
7344
+ // backdrop-filter has been observed to sample the page without dragging the
7345
+ // panel under the conversation. The panel's own fill is painted over it, so
7346
+ // a twin that fails costs the blur only.
7347
+ var panelGlassTwinEl = (!sheetOpen && C.glassOn && panelBlurH > 0)
7348
+ ? react_jsx_runtime.jsx("div", {
7349
+ "data-dqt-panel-glass": "1",
7350
+ style: mergeStyle(panelGeoStyle, {
7351
+ zIndex: Z_PANEL - 1,
7352
+ height: zLen(panelBlurH),
7353
+ background: "transparent",
7354
+ backdropFilter: C.panelBlur,
7355
+ WebkitBackdropFilter: C.panelBlur,
7356
+ opacity: panelOpacity,
7357
+ pointerEvents: "none",
7358
+ borderRadius: RADIUS_PANEL,
7359
+ clipPath: panelClip
7360
+ })
7361
+ })
7362
+ : null;
7363
+ // NO live blur on the docked panel itself — settled by experiment, not preference.
7364
+ // Every placement was tried in the real host: on the panel itself, on a
7365
+ // child layer, and on a sibling twin behind it. All three made the panel
7366
+ // disappear, and a child layer inside the panel's own `opacity` group
7367
+ // produced no blur at all (an element with opacity is a backdrop root, so
7368
+ // the filter sampled that element's own content). The panel lives inside a
7369
+ // clipping, fixed wrapper that the host's compositor keeps below the
7370
+ // conversation; anything that promotes a layer in there loses the stack.
7371
+ // So the panel keeps a translucent FILL (which reads as glass on its own:
7372
+ // the rim, the gutter shadow and the see-through body do the work) and the
7373
+ // live blur stays on the curtain, the transient surface — exactly the split
7374
+ // Windows makes between Mica (persistent) and Acrylic (transient).
6559
7375
  var panelShadowEl = react_jsx_runtime.jsx("div", {
6560
7376
  "data-dqt-panel-shadow": "1",
6561
7377
  style: mergeStyle(panelGeoStyle, {
@@ -6564,13 +7380,22 @@ window.__ModuleLoader__.load({
6564
7380
  opacity: panelOpacity,
6565
7381
  pointerEvents: "none",
6566
7382
  clipPath: shadowClip,
6567
- boxShadow: isDarkScheme()
7383
+ // the halo is the panel's lift; the offset line adds the gutter shadow on
7384
+ // the conversation side, so the sheet reads as hovering over the transcript
7385
+ boxShadow: (isDarkScheme()
6568
7386
  ? "0 0 24px -4px rgba(255, 255, 255, 0.16), 0 0 10px -2px rgba(255, 255, 255, 0.10)"
6569
- : "0 0 24px -4px rgba(0, 0, 0, 0.38), 0 0 10px -2px rgba(0, 0, 0, 0.26)"
7387
+ : "0 0 24px -4px rgba(0, 0, 0, 0.38), 0 0 10px -2px rgba(0, 0, 0, 0.26)")
7388
+ + (dockRight
7389
+ ? ", -12px 0 20px -14px rgba(0, 0, 0, 0.45)"
7390
+ : ", 12px 0 20px -14px rgba(0, 0, 0, 0.45)")
6570
7391
  })
6571
7392
  });
6572
7393
  var panelEl = react_jsx_runtime.jsx("div", {
6573
7394
  ref: panelRootRef,
7395
+ // a marker for the reader's own console checks (and for support): the panel
7396
+ // root used to have no attribute at all, so a lookup by class silently
7397
+ // returned null and read as "the panel is gone"
7398
+ "data-dqt-panel": "1",
6574
7399
  // the panel takes focus on any click inside it (except into a text field), so
6575
7400
  // ↑/↓/Enter/Esc keep working right after a mouse interaction instead of only
6576
7401
  // when the user tabs into the widget first
@@ -6593,7 +7418,9 @@ window.__ModuleLoader__.load({
6593
7418
  minHeight: 0,
6594
7419
  display: "flex",
6595
7420
  flexDirection: "column",
6596
- background: C.panelBg,
7421
+ // transparent on purpose: inside the curtain the CURTAIN is the glass, and
7422
+ // an opaque child would paint over it
7423
+ background: "transparent",
6597
7424
  border: "none",
6598
7425
  borderRadius: 0,
6599
7426
  zIndex: Z_PANEL,
@@ -6606,12 +7433,17 @@ window.__ModuleLoader__.load({
6606
7433
  } : mergeStyle(panelGeoStyle, {
6607
7434
  display: "flex",
6608
7435
  flexDirection: "column",
6609
- background: C.panelBg,
7436
+ // the filler colour is ALWAYS here (glassBg falls back to the opaque
7437
+ // panel colour where blur is unavailable); the blur rides on the child
7438
+ background: C.glassBg,
6610
7439
  border: "none",
6611
7440
  zIndex: Z_PANEL,
6612
7441
  overflow: "hidden",
6613
7442
  color: C.text,
6614
7443
  opacity: panelOpacity,
7444
+ // makes this element a stacking context so the glass layer's z-index:-1
7445
+ // stays inside it (behind the rows, in front of the page)
7446
+ isolation: "isolate",
6615
7447
  clipPath: panelClip,
6616
7448
  pointerEvents: chatViewActive ? "auto" : "none"
6617
7449
  }),
@@ -6656,6 +7488,13 @@ window.__ModuleLoader__.load({
6656
7488
  // the docked panel keeps its search band under the header; across the
6657
7489
  // curtain the same row lives at the top of the pushed-in search column
6658
7490
  sheetOpen ? null : searchRowEl,
7491
+ // the day strip: the "which day am I in" label, OUTSIDE the scroller.
7492
+ // A sticky label would have to be opaque to hide the rows passing under
7493
+ // it, and an opaque strip on a translucent surface is the band that reads
7494
+ // as a cut. Out here it needs no fill at all — it is just text on the
7495
+ // same material — while still staying put for as long as the reader
7496
+ // scrolls, and the list fades its first rows out before they reach it.
7497
+ dayStripEl,
6659
7498
  bodyRowEl,
6660
7499
  // "back to the newest row": floats over the list's lower-right corner and
6661
7500
  // fades in the moment the newest turns fall below the fold. Kept MOUNTED
@@ -6858,8 +7697,17 @@ window.__ModuleLoader__.load({
6858
7697
  // below the threshold it slides back into the divider (its usual motion), while the
6859
7698
  // droplet above the conversation stays put as the way in
6860
7699
  var edgeShown = !open && handleShown && chatViewActive && !narrowWindow;
6861
- var edgeLineLeft = viewport ? viewport.left : 0;
6862
- var edgeLineRight = viewport ? viewport.right + 52 : 60; // clear of the milestone rail
7700
+ // The pill is 12px centred in the 26px box, so its edge sits 7px inside the box
7701
+ // edge. Left dock: the panel opens at `area + 8`, so parking the box 1px out puts
7702
+ // the pill's left edge exactly on the panel's own left edge — one line.
7703
+ //
7704
+ // Right dock: deliberately NOT aligned. The panel is held 48px clear of the
7705
+ // area's right edge (the milestone rail lives there), so the pill — which hangs
7706
+ // on that edge — is 11px inside the panel's right edge by construction. Pulling
7707
+ // it out to meet the panel would push the pill into the rail, which is worse
7708
+ // than two lines nobody sees at the same time.
7709
+ var edgeLineLeft = (viewport ? viewport.left : 0) + 1;
7710
+ var edgeLineRight = (viewport ? viewport.right + 52 : 60); // clear of the milestone rail
6863
7711
  var edgeEl = (!open || !edgeGone) ? react_jsx_runtime.jsx("div", {
6864
7712
  style: {
6865
7713
  position: "fixed",
@@ -7058,6 +7906,11 @@ window.__ModuleLoader__.load({
7058
7906
  // takes pointer events, so its text can be selected and its pin can be clicked);
7059
7907
  // the hover card stays click-through except for its pin button.
7060
7908
  var renderPreviewCard = function (card, opts) {
7909
+ // the per-render palette (accent + glass) has to be handed in: this body
7910
+ // closed over the module-level C, whose glass fields are undefined, so the
7911
+ // card silently fell back to the opaque colour while everything else was
7912
+ // translucent
7913
+ var P = (opts && opts.palette) || C;
7061
7914
  var cardW = 264;
7062
7915
  var top = Math.max(8, Math.min(card.top - 4, window.innerHeight - 210));
7063
7916
  var left = dockRight
@@ -7088,7 +7941,7 @@ window.__ModuleLoader__.load({
7088
7941
  style: { display: "flex", alignItems: "flex-start", gap: 6 },
7089
7942
  children: [
7090
7943
  react_jsx_runtime.jsx("div", {
7091
- style: { flex: "1 1 auto", minWidth: 0, fontSize: 12, fontWeight: 600, color: C.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
7944
+ style: { flex: "1 1 auto", minWidth: 0, fontSize: 12, fontWeight: 600, color: P.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
7092
7945
  children: card.title
7093
7946
  }, "hc-t"),
7094
7947
  react_jsx_runtime.jsx("button", {
@@ -7111,8 +7964,8 @@ window.__ModuleLoader__.load({
7111
7964
  justifyContent: "center",
7112
7965
  border: "none",
7113
7966
  borderRadius: 6,
7114
- background: opts.pinned ? C.hover : "transparent",
7115
- color: opts.pinned ? C.text : C.muted,
7967
+ background: opts.pinned ? P.hover : "transparent",
7968
+ color: opts.pinned ? P.text : P.muted,
7116
7969
  cursor: "pointer",
7117
7970
  // the hover card is click-through except for this button, so reaching
7118
7971
  // for it does not depend on the row staying hovered
@@ -7128,17 +7981,17 @@ window.__ModuleLoader__.load({
7128
7981
  ];
7129
7982
  if (card.meta) {
7130
7983
  kids.push(react_jsx_runtime.jsx("div", {
7131
- style: { fontSize: 10, color: C.muted, opacity: 0.8, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
7984
+ style: { fontSize: 10, color: P.muted, opacity: 0.8, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
7132
7985
  children: card.meta
7133
7986
  }, "hc-m"));
7134
7987
  }
7135
7988
  kids.push(react_jsx_runtime.jsx("div", {
7136
- style: { fontSize: 11, lineHeight: "16px", color: C.text, opacity: 0.9, marginTop: 4, maxHeight: 116, overflow: "hidden" },
7989
+ style: { fontSize: 11, lineHeight: "16px", color: P.text, opacity: 0.9, marginTop: 4, maxHeight: 116, overflow: "hidden" },
7137
7990
  children: body || (card.ghost ? T("turn.loadTip") : "")
7138
7991
  }, "hc-b"));
7139
7992
  if (card.ghost) {
7140
7993
  kids.push(react_jsx_runtime.jsx("div", {
7141
- style: { fontSize: 10, color: C.muted, opacity: 0.7, marginTop: 4 },
7994
+ style: { fontSize: 10, color: P.muted, opacity: 0.7, marginTop: 4 },
7142
7995
  children: T("preview.truncated")
7143
7996
  }, "hc-n"));
7144
7997
  }
@@ -7159,12 +8012,18 @@ window.__ModuleLoader__.load({
7159
8012
  left: left,
7160
8013
  width: cardW,
7161
8014
  padding: "8px 10px",
7162
- borderRadius: 8,
8015
+ borderRadius: 10,
7163
8016
  cornerShape: "round",
7164
- background: C.panelBg,
7165
- color: C.text,
7166
- border: "1px solid " + C.panelBorder,
7167
- boxShadow: "0 8px 24px rgba(0,0,0,0.28)",
8017
+ // The same material as the panel: the same translucent fill, the same
8018
+ // rim, and the same SMALL radius of blur — the card floats in this
8019
+ // portal (no opacity group, nobody's parent), so its backdrop-filter
8020
+ // samples the page exactly like the curtain's.
8021
+ background: P.glassOn ? P.glassBg : P.panelBg,
8022
+ backdropFilter: P.glassOn ? P.panelBlur : undefined,
8023
+ WebkitBackdropFilter: P.glassOn ? P.panelBlur : undefined,
8024
+ color: P.text,
8025
+ border: "1px solid " + P.panelBorder,
8026
+ boxShadow: P.glassOn ? rimInset() + ", 0 8px 24px rgba(0,0,0,0.28)" : "0 8px 24px rgba(0,0,0,0.28)",
7168
8027
  // above the panel and the curtain — it is a sibling of both in this
7169
8028
  // portal, and both sit below it in the layer band we claim
7170
8029
  zIndex: Z_HOVER + (opts.pinned ? 1 : 0),
@@ -7182,21 +8041,70 @@ window.__ModuleLoader__.load({
7182
8041
  // and it only leaves the screen through the same fade the hover card uses.
7183
8042
  var pinnedVisible = !!(pinnedCard && open);
7184
8043
  var pinnedEl = pinnedVisible
7185
- ? renderPreviewCard(pinnedCard, { pinned: true, closing: pinnedClosing })
8044
+ ? renderPreviewCard(pinnedCard, { pinned: true, closing: pinnedClosing, palette: C })
7186
8045
  : null;
7187
8046
  var hoverEl = null;
7188
- if (hoverCard && open) hoverEl = renderPreviewCard(hoverCard, { pinned: false, closing: hoverClosing });
8047
+ if (hoverCard && open) hoverEl = renderPreviewCard(hoverCard, { pinned: false, closing: hoverClosing, palette: C });
7189
8048
 
7190
8049
  // ---- the row menu ---------------------------------------------------------
7191
8050
  // Small, fixed-position, keyboard-reachable: ↑/↓ move, Enter runs, Esc closes.
7192
8051
  var rowMenuEl = null;
7193
8052
  if (rowMenu && open) {
7194
8053
  var menuW = 208;
7195
- var menuH = 76;
7196
- var menuLeft = Math.max(8, Math.min(rowMenu.x, window.innerWidth - menuW - 8));
7197
- var menuTop = Math.max(8, Math.min(rowMenu.y, window.innerHeight - menuH - 8));
8054
+ var menuLeft;
8055
+ var menuTop;
8056
+ // NOTE: the height (and therefore the clamped top) can only be known once
8057
+ // the item list exists — computing it above the list dereferenced a hoisted
8058
+ // `var` and threw on every open.
8059
+ // Two sources open this menu: an outline row and a time row (the group
8060
+ // header). A time row has no preview to pin and no single heading to copy,
8061
+ // so those two entries are absent there; everything else is identical.
8062
+ var menuIsGroup = !!(rowMenu.info && rowMenu.info.kind === "group");
8063
+ var menuCanCopySection = menuIsGroup
8064
+ ? !!(rowMenu.info && rowMenu.info.canCopy)
8065
+ : !!(rowMenu.info && rowMenu.info.hasKids);
7198
8066
  var menuItems = [
7199
8067
  {
8068
+ id: "goto",
8069
+ label: T("menu.goto"),
8070
+ run: function () {
8071
+ var key = rowMenu.key;
8072
+ var idx = rowMenu.idx;
8073
+ closeRowMenu();
8074
+ jump(key, idx === undefined ? 0 : idx);
8075
+ }
8076
+ },
8077
+ {
8078
+ id: "mark",
8079
+ label: isMarked(rowMenu.key, rowMenu.idx) ? T("menu.unmark") : T("menu.mark"),
8080
+ run: function () {
8081
+ toggleMark(rowMenu.key, rowMenu.idx, rowMenu.info && rowMenu.info.title,
8082
+ menuIsGroup ? 0 : (rowMenu.info && rowMenu.info.level));
8083
+ closeRowMenu();
8084
+ }
8085
+ },
8086
+ { id: "sep1" },
8087
+ menuIsGroup ? null : {
8088
+ id: "copyTitle",
8089
+ label: copiedId === "copyTitle" ? T("menu.copied") : T("menu.copyTitle"),
8090
+ run: function () {
8091
+ copyText((rowMenu.info && rowMenu.info.title) || "");
8092
+ flashCopied("copyTitle");
8093
+ }
8094
+ },
8095
+ // only when there really is an outline below it, so the entry never
8096
+ // copies the same text the entry above it already copies
8097
+ menuCanCopySection ? {
8098
+ id: "copySection",
8099
+ label: copiedId === "copySection" ? T("menu.copied") : T("menu.copySection"),
8100
+ run: function () {
8101
+ var info = rowMenu.info;
8102
+ copyText(info && typeof info.mdOf === "function" ? info.mdOf() : ((info && info.title) || ""));
8103
+ flashCopied("copySection");
8104
+ }
8105
+ } : null,
8106
+ { id: "sep2" },
8107
+ menuIsGroup ? null : {
7200
8108
  id: "pin",
7201
8109
  label: isPinned(rowMenu.key) ? T("menu.unpin") : T("menu.pin"),
7202
8110
  run: function () {
@@ -7226,7 +8134,23 @@ window.__ModuleLoader__.load({
7226
8134
  if (typeof endJumpRow === "function") endJumpRow(key, idx === undefined ? 0 : idx);
7227
8135
  }
7228
8136
  }
7229
- ];
8137
+ ].filter(function (it, i, arr) {
8138
+ // drop the entries this source does not offer, then collapse the
8139
+ // separators that ended up adjacent or at either end
8140
+ if (!it) return false;
8141
+ if (it.id.indexOf("sep") !== 0) return true;
8142
+ var prev = null;
8143
+ for (var p = i - 1; p >= 0; p--) { if (arr[p]) { prev = arr[p]; break; } }
8144
+ var next = null;
8145
+ for (var q = i + 1; q < arr.length; q++) { if (arr[q]) { next = arr[q]; break; } }
8146
+ if (!prev || !next) return false;
8147
+ if (prev.id.indexOf("sep") === 0) return false;
8148
+ return true;
8149
+ });
8150
+ // now that the list exists: keep the menu inside the viewport
8151
+ var menuH = 12 + menuItems.length * 30;
8152
+ menuLeft = Math.max(8, Math.min(rowMenu.x, window.innerWidth - menuW - 8));
8153
+ menuTop = Math.max(8, Math.min(rowMenu.y, window.innerHeight - menuH - 8));
7230
8154
  rowMenuEl = react_jsx_runtime.jsx("div", {
7231
8155
  "data-dqt-menu": "on",
7232
8156
  className: "dqt-rowmenu" + (menuClosing ? " dqt-rowmenu-closing" : ""),
@@ -7247,6 +8171,12 @@ window.__ModuleLoader__.load({
7247
8171
  pointerEvents: menuClosing ? "none" : "auto"
7248
8172
  },
7249
8173
  children: menuItems.map(function (item) {
8174
+ if (item.id.indexOf("sep") === 0) {
8175
+ return react_jsx_runtime.jsx("div", {
8176
+ "data-dqt-menu-sep": "1",
8177
+ style: { height: 1, background: C.panelBorder, margin: "4px 6px", opacity: 0.7 }
8178
+ }, "menu-" + item.id);
8179
+ }
7250
8180
  return react_jsx_runtime.jsx("button", {
7251
8181
  role: "menuitem",
7252
8182
  "data-dqt-menu-item": item.id,
@@ -7283,6 +8213,30 @@ window.__ModuleLoader__.load({
7283
8213
  // bottom shadow lives (the clip would otherwise cut it off).
7284
8214
  var SHEET_SHADOW_ROOM = 24;
7285
8215
  var sheetUp = sheetOpen && chatViewActive;
8216
+ // The curtain's resize edge: a 10px strip along its bottom, with the pill
8217
+ // that says "this can be pulled". Only rendered while the curtain is up.
8218
+ var sheetGripEl = react_jsx_runtime.jsx("div", {
8219
+ "data-dqt-sheet-grip": "1",
8220
+ onPointerDown: sheetResizeStart,
8221
+ onPointerMove: sheetResizeMove,
8222
+ onPointerUp: sheetResizeEnd,
8223
+ onPointerCancel: sheetResizeEnd,
8224
+ title: T("sheet.resize"),
8225
+ style: {
8226
+ flex: "none", height: 10, width: "100%", cursor: "row-resize",
8227
+ touchAction: "none", display: "flex", alignItems: "center",
8228
+ justifyContent: "center", background: "transparent"
8229
+ },
8230
+ children: react_jsx_runtime.jsx("span", {
8231
+ style: {
8232
+ display: "block", width: 42, height: 4, borderRadius: 999,
8233
+ cornerShape: "round",
8234
+ // the one decorative spot that follows the theme colour: the grip is
8235
+ // the curtain's own control, and a neutral grey pill read as disabled
8236
+ background: C.accent, opacity: 0.35
8237
+ }
8238
+ })
8239
+ });
7286
8240
  var sheetShellEl = react_jsx_runtime.jsx("div", {
7287
8241
  key: "sheet-shell",
7288
8242
  className: "dqt-sheet-clip",
@@ -7333,21 +8287,37 @@ window.__ModuleLoader__.load({
7333
8287
  height: 0,
7334
8288
  overflow: "visible"
7335
8289
  },
7336
- children: react_jsx_runtime.jsx("div", {
8290
+ // the panel's blur twin rides here, as a sibling of the curtain: the only
8291
+ // depth in this subtree where a backdrop-filter has been seen to work
8292
+ children: [panelGlassTwinEl, react_jsx_runtime.jsx("div", {
7337
8293
  ref: sheetRef,
7338
8294
  className: sheetUp ? "dqt-sheet dqt-sheet-open" : "dqt-sheet-idle",
7339
8295
  "data-sheet": sheetUp ? "on" : undefined,
7340
8296
  style: {
7341
8297
  width: sheetUp ? "100%" : 0,
7342
- // the bottom leaves the shadow room the clip box grew by
7343
- height: sheetUp ? "calc(100% - " + SHEET_SHADOW_ROOM + "px)" : 0,
8298
+ // the bottom leaves the shadow room the clip box grew by; the fraction
8299
+ // is the reader's own height (dragged from the bottom edge, remembered)
8300
+ height: sheetUp
8301
+ ? "calc((100% - " + SHEET_SHADOW_ROOM + "px) * " + (sheetDragFrac === null ? prefs.sheetHeight : sheetDragFrac) + ")"
8302
+ : 0,
7344
8303
  // border-box so the 1px edges stay INSIDE the measured height: with the
7345
8304
  // old content-box the bottom pair sat below it and the clip cut them
7346
8305
  // away, which is why the bottom edge had no visible line
7347
8306
  boxSizing: "border-box",
7348
8307
  display: "flex",
7349
8308
  flexDirection: "column",
7350
- background: C.panelBg,
8309
+ // The curtain is the transient surface (Windows' "Acrylic" case): the
8310
+ // live blur belongs here, where it is dismissed rather than lived in.
8311
+ // GATED ON sheetUp — this element is ALSO the docked panel's parent
8312
+ // while the curtain is away (height 0, yet still an ancestor), and a
8313
+ // backdrop-filter here promoted the whole subtree and dropped the
8314
+ // panel beneath the conversation. That was the real "panel disappears"
8315
+ // bug: the panel itself had no filter at all in that build.
8316
+ background: sheetUp ? C.sheetBg : "transparent",
8317
+ // L: a light/dark switch eases the material instead of snapping it
8318
+ transition: "background-color 0.25s ease",
8319
+ backdropFilter: sheetUp && C.glassOn ? C.sheetBlur : undefined,
8320
+ WebkitBackdropFilter: sheetUp && C.glassOn ? C.sheetBlur : undefined,
7351
8321
  color: C.text,
7352
8322
  border: "none",
7353
8323
  borderTop: sheetUp ? "1px solid " + C.panelBorder : "none",
@@ -7358,17 +8328,25 @@ window.__ModuleLoader__.load({
7358
8328
  // no dimming layer — the conversation below stays legible and usable.
7359
8329
  // The outer shadow is the bottom edge's own: it follows the rounded
7360
8330
  // bottom corners and stops before the vertical seams.
7361
- borderRadius: "0 0 12px 12px",
7362
- boxShadow: sheetUp ? C.sheetShadow : "none",
8331
+ borderRadius: "0 0 " + RADIUS_SHEET + "px " + RADIUS_SHEET + "px",
8332
+ // the drop shadow lifts the sheet off the conversation; the inset pair
8333
+ // is the rim that says "this is a surface, not a haze"
8334
+ boxShadow: sheetUp ? (rimInset() + ", " + C.sheetShadow) : "none",
7363
8335
  overflow: "hidden"
7364
8336
  },
7365
- children: sheetOpen ? panelEl : [panelShadowEl, panelEl]
7366
- })
8337
+ // inside the curtain the SHEET is the glass (it has no opacity of its own,
8338
+ // so its backdrop-filter samples the page): no second twin here.
8339
+ // The grip is the curtain's own bottom edge: drag it to set the height.
8340
+ children: sheetOpen ? [panelEl, sheetGripEl] : [panelShadowEl, panelEl]
8341
+ })]
7367
8342
  });
7368
8343
 
7369
8344
  return react_dom.createPortal(
7370
8345
  react_jsx_runtime.jsx(ErrorBoundary, {
7371
- children: [sheetShellEl, edgeEl, topHandleEl, pinnedEl, hoverEl, rowMenuEl]
8346
+ // panelGlassTwinEl sits OUTSIDE the clipping wrapper on purpose: it is
8347
+ // the only place a backdrop-filter can sample the page without dragging
8348
+ // the docked panel under the conversation
8349
+ children: [panelGlassTwinEl, sheetShellEl, edgeEl, topHandleEl, pinnedEl, hoverEl, rowMenuEl]
7372
8350
  }),
7373
8351
  document.body
7374
8352
  );
@@ -7463,7 +8441,16 @@ window.__ModuleLoader__.load({
7463
8441
  ghost: false,
7464
8442
  // what pinning and the row menu need to act on this very row
7465
8443
  key: n.key,
7466
- idx: n.idx
8444
+ idx: n.idx,
8445
+ level: n.level || 1,
8446
+ kind: "row",
8447
+ // "copy this section" is only offered when there IS a section below it:
8448
+ // for a heading with no sub-headings the entry would copy nothing but the
8449
+ // heading itself, which is what "copy heading" already does.
8450
+ hasKids: !!(n.children && n.children.length),
8451
+ // Built only if the reader actually picks "copy this section": walking a
8452
+ // subtree on every row of every render would be wasted work on a long log.
8453
+ mdOf: function () { return markdownOfNode(n).join("\n"); }
7467
8454
  };
7468
8455
  var rows;
7469
8456
  if (wide) {
@@ -7532,6 +8519,69 @@ window.__ModuleLoader__.load({
7532
8519
  // round backdrop appears, so the two boxes used to sit flush against each other
7533
8520
  // (and on a long row the highlight ran underneath the button). This layer stops
7534
8521
  // END_ZONE short of that edge, leaving a clear gap.
8522
+ // G: a hairline guide at this row's own indent, so H2/H3 read as children
8523
+ // without moving the text. Only rows below H1 carry one; the guide sits just
8524
+ // left of where the row's text starts.
8525
+ // The guide column depends on the LEVEL only. It must not share the text's
8526
+ // base inset: that inset differs for rows with children (2/4px) and without
8527
+ // (6/8px), which put rows of the same level 4px apart and made the lines
8528
+ // visibly ragged.
8529
+ var guideStep = wide ? 16 : 12;
8530
+ var guideOrigin = (wide ? 8 : 6) + MARK_INSET;
8531
+ if (n.level > 1) {
8532
+ rows.unshift(react_jsx_runtime.jsx("div", {
8533
+ "data-dqt-guide": "1",
8534
+ style: {
8535
+ position: "absolute",
8536
+ left: guideOrigin + (n.level - 2) * guideStep,
8537
+ top: 0,
8538
+ bottom: 0,
8539
+ width: 1,
8540
+ background: C.panelBorder,
8541
+ pointerEvents: "none",
8542
+ // the row leans in on hover; the guide column must not, or the line
8543
+ // visibly slides sideways every time the pointer crosses a row
8544
+ transition: "transform 0.15s ease"
8545
+ }
8546
+ }, uid + "-gd"));
8547
+ }
8548
+ // B: the bookmark. A ribbon, not a dot: a tab whose left edge is flush with
8549
+ // the row's left margin and whose right edge is cut inwards to a point — the
8550
+ // shape of a marker sticking out of the page, so it reads as "saved" rather
8551
+ // than as a bullet.
8552
+ //
8553
+ // ALWAYS RENDERED, faded by opacity: a layer that is created and destroyed pops
8554
+ // in and out, and saving a place is exactly the kind of small action that should
8555
+ // breathe (everything else in this panel fades).
8556
+ //
8557
+ // The colour is a concrete hex taken from the theme table, not a `var()`:
8558
+ // an SVG fill attribute silently rejects a var and paints black, and a colour
8559
+ // that has to be resolved by the compositor inside a clipped layer is one more
8560
+ // chance for that to happen again.
8561
+ var marked = !!(extra && extra.isMarked && extra.isMarked(n.key, n.idx));
8562
+ rows.unshift(react_jsx_runtime.jsx("div", {
8563
+ "data-dqt-mark": marked ? "1" : undefined,
8564
+ style: {
8565
+ position: "absolute",
8566
+ // Inside the row's own box on purpose: the row clips its content (it
8567
+ // needs `overflow: hidden` for the title's ellipsis), so a ribbon hung
8568
+ // outside it was cut down to a sliver. The row itself is pulled left
8569
+ // instead (see marginLeft below), which is what puts this at the panel
8570
+ // edge while still being painted by the row.
8571
+ left: 0,
8572
+ top: "50%",
8573
+ transform: "translateY(-50%)",
8574
+ width: MARK_W,
8575
+ height: 12,
8576
+ background: (extra && extra.markColor) || C.accent,
8577
+ // flat on the panel side, a concave point cut into the other side
8578
+ clipPath: "polygon(0 0, 100% 0, 70% 50%, 100% 100%, 0 100%)",
8579
+ WebkitClipPath: "polygon(0 0, 100% 0, 70% 50%, 100% 100%, 0 100%)",
8580
+ opacity: marked ? 1 : 0,
8581
+ transition: "opacity 0.24s ease",
8582
+ pointerEvents: "none"
8583
+ }
8584
+ }, uid + "-mk"));
7535
8585
  rows.unshift(react_jsx_runtime.jsx("div", {
7536
8586
  "data-dqt-hl": "1",
7537
8587
  style: {
@@ -7555,6 +8605,11 @@ window.__ModuleLoader__.load({
7555
8605
  if (hl) hl.style.background = C.hover;
7556
8606
  var b = e.currentTarget.querySelector("[data-dqt-end]");
7557
8607
  if (b) b.style.opacity = "1";
8608
+ // I: the row leans in by one pixel — enough to feel alive, small enough
8609
+ // that the text never looks like it jumped
8610
+ e.currentTarget.style.transform = "translateX(1px)";
8611
+ var gd = e.currentTarget.querySelector("[data-dqt-guide]");
8612
+ if (gd) gd.style.transform = "translateX(-1px)";
7558
8613
  if (extra && extra.hoverStart) extra.hoverStart(hoverInfo, e.currentTarget);
7559
8614
  },
7560
8615
  onMouseLeave: function (e) {
@@ -7562,6 +8617,9 @@ window.__ModuleLoader__.load({
7562
8617
  if (hl) hl.style.background = "transparent";
7563
8618
  var b = e.currentTarget.querySelector("[data-dqt-end]");
7564
8619
  if (b) b.style.opacity = "0";
8620
+ e.currentTarget.style.transform = "translateX(0)";
8621
+ var gd2 = e.currentTarget.querySelector("[data-dqt-guide]");
8622
+ if (gd2) gd2.style.transform = "translateX(0)";
7565
8623
  if (extra && extra.hoverEnd) extra.hoverEnd();
7566
8624
  },
7567
8625
  onContextMenu: function (e) {
@@ -7578,10 +8636,20 @@ window.__ModuleLoader__.load({
7578
8636
  position: "relative",
7579
8637
  padding: dense ? (wide ? "1px 8px" : "0px 6px") : wide ? "4px 8px" : "2px 6px",
7580
8638
  paddingRight: END_ZONE,
7581
- paddingLeft: (hasChildren ? (wide ? 4 : 2) : (wide ? 8 : 6)) + (n.level - 1) * (wide ? 16 : 12),
8639
+ // the ribbon gutter is always reserved (see MARK_INSET): saving a row must
8640
+ // never move the text of that row, or of every other row after it
8641
+ paddingLeft: MARK_INSET + (hasChildren ? (wide ? 4 : 2) : (wide ? 8 : 6)) + (n.level - 1) * (wide ? 16 : 12),
7582
8642
  margin: dense ? "0 0" : "1px 0",
8643
+ // Reach back over the group box's left padding so the ribbon gutter sits
8644
+ // at the surface's edge. The group clips its contents, so this is as far
8645
+ // left as anything on a row can go — and the row is still inside the
8646
+ // group's box, so nothing is cut.
8647
+ marginLeft: -(wide ? 8 : 7),
7583
8648
  borderRadius: 6,
7584
8649
  cursor: "pointer",
8650
+ // I: a 1px nudge on hover (no reflow — transform only), plus the panel's
8651
+ // own background transition so a light/dark switch eases instead of snapping
8652
+ transition: "transform 0.15s ease, background-color 0.25s ease",
7585
8653
  fontSize: n.level <= 2 ? (wide ? 16.5 : 13) : (wide ? 15.5 : 12),
7586
8654
  color: n.level <= 2 ? C.text : C.muted,
7587
8655
  fontWeight: n.level <= 2 ? 600 : 400,
@@ -7858,6 +8926,10 @@ window.__ModuleLoader__.load({
7858
8926
  var lastKey = (g.msgs && g.msgs.length > 0) ? g.msgs[g.msgs.length - 1].key
7859
8927
  : (g.failure ? g.failure.key : "");
7860
8928
  return react_jsx_runtime.jsx("div", {
8929
+ // the day strip above the list reads these two attributes on scroll: the
8930
+ // label that has scrolled past the top edge is the one it shows
8931
+ "data-dqt-grouplabel": "1",
8932
+ "data-dqt-day": g.time !== undefined ? String(fmtStampOf(g.time)) : "",
7861
8933
  onMouseEnter: function (e) {
7862
8934
  var b = e.currentTarget.querySelector("[data-dqt-end]");
7863
8935
  if (b) b.style.opacity = "1";
@@ -7866,6 +8938,26 @@ window.__ModuleLoader__.load({
7866
8938
  var b = e.currentTarget.querySelector("[data-dqt-end]");
7867
8939
  if (b) b.style.opacity = "0";
7868
8940
  },
8941
+ // The time row takes the same menu as an outline row, minus the two entries
8942
+ // that need a single preview card / single heading (pin, copy heading).
8943
+ // "Copy this section" here means every heading under this turn.
8944
+ onContextMenu: function (e) {
8945
+ if (!extra || !extra.rowMenu) return;
8946
+ var anchorKey = replyKey || g.userKey || "";
8947
+ extra.rowMenu(e, {
8948
+ title: (g.time ? String(g.time) : "") + (g.userText ? " · " + g.userText : ""),
8949
+ sub: "",
8950
+ preview: "",
8951
+ meta: "",
8952
+ ghost: false,
8953
+ key: anchorKey,
8954
+ idx: 0,
8955
+ level: 0,
8956
+ kind: "group",
8957
+ canCopy: !!(g.headings && g.headings.length),
8958
+ mdOf: function () { return markdownOfGroup(g); }
8959
+ }, anchorKey, 0);
8960
+ },
7869
8961
  style: {
7870
8962
  // ONE row geometry for every group header, whether or not the turn has
7871
8963
  // headings: same box, same 18px rhythm, same left inset, so the time
@@ -7883,15 +8975,20 @@ window.__ModuleLoader__.load({
7883
8975
  display: "flex",
7884
8976
  alignItems: "center",
7885
8977
  minWidth: 0,
7886
- // sticky: while you scroll the outline, the header of the group you are
7887
- // inside stays pinned at the top. It must be opaque to cover the rows
7888
- // scrolling underneath, so its base colour is the panel background in both
7889
- // states — the "current group" tint rides on its own layer inside, which
7890
- // can FADE (a background-image gradient cannot be transitioned).
7891
- position: "sticky",
7892
- top: 0,
7893
- zIndex: 2,
7894
- background: C.panelBg
8978
+ // NOT sticky since the surface became translucent (0.10.0): a label that
8979
+ // has to occlude the rows scrolling under it needs an opaque fill, and an
8980
+ // opaque fill on a see-through panel is exactly the horizontal band that
8981
+ // reads as a cut. Letting the label scroll away keeps ONE transparency
8982
+ // across the whole list; the current date moved into the title bar (which
8983
+ // is chrome, and therefore legitimately solid).
8984
+ // The "current group" tint still rides on its own layer inside the row,
8985
+ // which can FADE (a background-image gradient cannot be transitioned).
8986
+ // RELATIVE, not static: that tint layer is `position:absolute; inset:0`,
8987
+ // so it needs this row as its containing block. As a static row the tint
8988
+ // resolved against the panel itself and washed the whole list in the
8989
+ // accent colour for a frame (then the active group settled and it went).
8990
+ position: "relative",
8991
+ background: "transparent"
7895
8992
  },
7896
8993
  children: [
7897
8994
  react_jsx_runtime.jsx("div", {
@@ -8133,10 +9230,16 @@ window.__ModuleLoader__.load({
8133
9230
  borderTop: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
8134
9231
  borderRight: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
8135
9232
  borderBottom: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
8136
- borderLeft: "3px solid " + (isActive ? C.groupEdge : "transparent"),
9233
+ // The accent bar is a LAYER, not a border: it needs to be a gradient and
9234
+ // to fade with the rest of the box. It cannot sit in a transparent left
9235
+ // border, though — this box carries `content-visibility: auto`, which
9236
+ // implies `contain: paint`, and paint containment clips descendants to
9237
+ // the PADDING box (the bar was clipped away entirely). So the 3px the
9238
+ // border used to take is padding instead, and the bar lives inside it.
9239
+ // Same content position: 5px bar + 2px padding == 7px padding.
9240
+ paddingLeft: 7,
8137
9241
  borderRadius: 6,
8138
9242
  margin: dense ? "0 -5px" : "2px -5px",
8139
- paddingLeft: 2,
8140
9243
  // Long-session virtualization: every offscreen group is skipped by the
8141
9244
  // renderer (no layout, no paint) until it nears the viewport, while its
8142
9245
  // DOM stays in place — so the row queries the follow, the keyboard
@@ -8146,15 +9249,88 @@ window.__ModuleLoader__.load({
8146
9249
  // While a surface is ANIMATING the hold lifts the skip entirely:
8147
9250
  // a compositor-driven slide does not re-evaluate the skip state, so
8148
9251
  // an opening surface would otherwise stay blank until it lands.
9252
+ position: "relative",
8149
9253
  contentVisibility: extra && extra.virtHold ? "visible" : "auto",
8150
9254
  containIntrinsicSize: "auto " + (dense ? 80 : 96) + "px"
8151
9255
  },
8152
- children: items
9256
+ children: [
9257
+ // H: the accent bar, as a layer so it can be a gradient AND fade with the
9258
+ // rest of the box. It sits inside the 3px transparent left border, so the
9259
+ // row geometry is identical in both states.
9260
+ react_jsx_runtime.jsx("div", {
9261
+ key: "g-bar",
9262
+ style: {
9263
+ position: "absolute",
9264
+ left: 0,
9265
+ top: 0,
9266
+ bottom: 0,
9267
+ width: 5,
9268
+ background: "linear-gradient(to bottom, " + C.groupEdge + ", " + C.groupEdgeSoft + ")",
9269
+ opacity: isActive ? 1 : 0,
9270
+ transition: "opacity 0.4s ease",
9271
+ pointerEvents: "none"
9272
+ }
9273
+ })
9274
+ ].concat(items)
8153
9275
  }, "g-" + gi));
8154
9276
  }
8155
9277
  return out;
8156
9278
  }
8157
9279
 
9280
+ // A heading and everything under it, as Markdown. Only headings: this is the
9281
+ // outline below this entry, not a transcript of the section's prose. The entry
9282
+ // is offered only when there IS something below (see the row menu), so an empty
9283
+ // result can never be pasted by accident.
9284
+ function markdownOfNode(n) {
9285
+ var out = [];
9286
+ var lvl = Math.min(6, Math.max(1, n.level || 1));
9287
+ var hashes = "";
9288
+ for (var h = 0; h < lvl; h++) hashes += "#";
9289
+ out.push(hashes + " " + (n.title || ""));
9290
+ var kids = n.children || [];
9291
+ for (var i = 0; i < kids.length; i++) {
9292
+ out = out.concat(markdownOfNode(kids[i]));
9293
+ }
9294
+ return out;
9295
+ }
9296
+ // The same thing for a whole turn: every heading under one time row, in order.
9297
+ function markdownOfGroup(g) {
9298
+ var hs = (g && g.headings) || [];
9299
+ var out = [];
9300
+ for (var i = 0; i < hs.length; i++) {
9301
+ var lvl = Math.min(6, Math.max(1, hs[i].level || 1));
9302
+ var hashes = "";
9303
+ for (var h = 0; h < lvl; h++) hashes += "#";
9304
+ out.push(hashes + " " + (hs[i].title || ""));
9305
+ }
9306
+ return out.join("\n");
9307
+ }
9308
+ // Put text on the clipboard. The async API needs a secure context (the panel
9309
+ // runs on the host page, which is loopback or https) and is unavailable on a
9310
+ // plain-http remote visit, so the execCommand path is the fallback rather than
9311
+ // an afterthought.
9312
+ var copyText = function (text) {
9313
+ try {
9314
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
9315
+ navigator.clipboard.writeText(String(text));
9316
+ return true;
9317
+ }
9318
+ } catch (e) { /* fall through to the legacy path */ }
9319
+ try {
9320
+ var ta = document.createElement("textarea");
9321
+ ta.value = String(text);
9322
+ ta.setAttribute("readonly", "");
9323
+ ta.style.position = "fixed";
9324
+ ta.style.top = "-1000px";
9325
+ ta.style.opacity = "0";
9326
+ document.body.appendChild(ta);
9327
+ if (ta.select) ta.select();
9328
+ var ok = typeof document.execCommand === "function" ? document.execCommand("copy") : false;
9329
+ document.body.removeChild(ta);
9330
+ return !!ok;
9331
+ } catch (e) { return false; }
9332
+ };
9333
+
8158
9334
  // Host-facing locale namespace: the slot declares `locale: "dsh-quick-toc"`, and
8159
9335
  // registering BOTH tables under the same keys is what lets the panel follow the
8160
9336
  // host's language: `T` asks the host translation first and only falls back to the
@@ -8216,6 +9392,43 @@ window.__ModuleLoader__.load({
8216
9392
  return seg([["on", T("settings.on")], ["off", T("settings.off")]], on ? "on" : "off",
8217
9393
  function (v) { onToggle(v === "on"); });
8218
9394
  };
9395
+ // Theme colour dots: the choice IS a colour, so it looks like one. Inline
9396
+ // styles (no stylesheet dependency): a 22px dot per option, the selected
9397
+ // one ringed. The dot shows the vivid hue; text roles use the readable
9398
+ // solid from the same option (see ACCENTS). Labels are listed statically
9399
+ // (like every seg row) so the dictionary check sees each key.
9400
+ var accentDots = function (current, onPick) {
9401
+ var pairs = [["blue", T("settings.accent.blue")], ["red", T("settings.accent.red")],
9402
+ ["yellow", T("settings.accent.yellow")], ["green", T("settings.accent.green")],
9403
+ ["purple", T("settings.accent.purple")], ["pink", T("settings.accent.pink")]];
9404
+ return react_jsx_runtime.jsx("div", {
9405
+ className: "dqt-dots",
9406
+ role: "radiogroup",
9407
+ "aria-label": T("settings.accent"),
9408
+ style: { display: "flex", gap: "10px", alignItems: "center", padding: "2px" },
9409
+ children: pairs.map(function (pair) {
9410
+ var id = pair[0];
9411
+ var on = current === id;
9412
+ var dot = (ACCENTS[id] || ACCENTS.blue).dot;
9413
+ return react_jsx_runtime.jsx("button", {
9414
+ type: "button",
9415
+ role: "radio",
9416
+ "aria-checked": on,
9417
+ title: pair[1],
9418
+ "aria-label": pair[1],
9419
+ disabled: !writable,
9420
+ onClick: function () { onPick(id); },
9421
+ className: "dqt-dot" + (on ? " dqt-dotOn" : ""),
9422
+ style: {
9423
+ width: "22px", height: "22px", borderRadius: "50%", padding: 0,
9424
+ border: "none", cursor: writable ? "pointer" : "default",
9425
+ background: dot, opacity: writable ? 1 : 0.4,
9426
+ boxShadow: on ? "0 0 0 2px var(--dsw-alias-bg-base, #fff), 0 0 0 4px " + dot : "inset 0 0 0 1px rgba(128,128,128,0.35)"
9427
+ }
9428
+ }, id);
9429
+ })
9430
+ });
9431
+ };
8219
9432
  // Percentage sliders (the panel scale and the handle position) — the same
8220
9433
  // control shape as the fonttune card's slider. Dragging only moves a LOCAL
8221
9434
  // value: the settings document is written once the pointer is released (a
@@ -8230,7 +9443,9 @@ window.__ModuleLoader__.load({
8230
9443
  sliderAwaiting.current[field] = pct;
8231
9444
  commit(field === "zoom" ? { zoom: pct / 100 }
8232
9445
  : field === "sheetZoom" ? { sheetZoom: pct / 100 }
8233
- : { handle: pct / 100 });
9446
+ // this one is stored as the 0..100 it reads as, not as a ratio
9447
+ : field === "glassStrength" ? { glassStrength: pct }
9448
+ : { handle: pct / 100 });
8234
9449
  };
8235
9450
  react.useEffect(function () {
8236
9451
  var a = sliderAwaiting.current;
@@ -8238,8 +9453,9 @@ window.__ModuleLoader__.load({
8238
9453
  if (a.zoom !== undefined && Math.round(prefs.zoom * 100) === a.zoom) { delete a.zoom; settled = true; }
8239
9454
  if (a.sheetZoom !== undefined && Math.round(prefs.sheetZoom * 100) === a.sheetZoom) { delete a.sheetZoom; settled = true; }
8240
9455
  if (a.handle !== undefined && Math.round(prefs.handle * 100) === a.handle) { delete a.handle; settled = true; }
9456
+ if (a.glassStrength !== undefined && Math.round(prefs.glassStrength) === a.glassStrength) { delete a.glassStrength; settled = true; }
8241
9457
  if (settled) setSliderDrag(null);
8242
- }, [prefs.zoom, prefs.sheetZoom, prefs.handle]);
9458
+ }, [prefs.zoom, prefs.sheetZoom, prefs.handle, prefs.glassStrength]);
8243
9459
  react.useEffect(function () {
8244
9460
  if (sliderDrag === null) return undefined;
8245
9461
  var release = function () {
@@ -8291,6 +9507,7 @@ window.__ModuleLoader__.load({
8291
9507
  var zoomControl = sliderCtl("zoom", "settings.zoom", 50, 200, 5, Math.round(prefs.zoom * 100));
8292
9508
  var sheetZoomControl = sliderCtl("sheetZoom", "settings.sheetZoom", 50, 200, 5, Math.round(prefs.sheetZoom * 100));
8293
9509
  var handleControl = sliderCtl("handle", "settings.handle", 0, 100, 1, Math.round(prefs.handle * 100));
9510
+ var glassStrengthControl = sliderCtl("glassStrength", "settings.glassStrength", 0, 100, 5, clampPct(prefs.glassStrength, 60));
8294
9511
  var field = function (key, label, hint, control, inline, last) {
8295
9512
  var overridden = Object.prototype.hasOwnProperty.call(user, key);
8296
9513
  var reset = overridden ? react_jsx_runtime.jsx("button", {
@@ -8353,6 +9570,12 @@ window.__ModuleLoader__.load({
8353
9570
  field("density", T("settings.density"), "",
8354
9571
  seg([["comfy", T("settings.density.comfy")], ["compact", T("settings.density.compact")]], prefs.density,
8355
9572
  function (v) { commit({ density: v }); }), true),
9573
+ field("accent", T("settings.accent"), "",
9574
+ accentDots(prefs.accent, function (v) { commit({ accent: v }); }), true),
9575
+ field("glass", T("settings.glass"),
9576
+ T("settings.glass.tip") + (autoGlassOff ? " " + T("settings.glass.autoOff") : ""),
9577
+ onoff(prefs.glass, function (v) { commit({ glass: v }); }), true),
9578
+ field("glassStrength", T("settings.glassStrength"), T("settings.glassStrength.tip"), glassStrengthControl, false),
8356
9579
  field("zoom", T("settings.zoom"), T("settings.zoom.tip"), zoomControl, false),
8357
9580
  field("sheetZoom", T("settings.sheetZoom"), T("settings.sheetZoom.tip"), sheetZoomControl, false),
8358
9581
  field("handle", T("settings.handle"), T("settings.handle.tip"), handleControl, false),
@@ -8697,6 +9920,10 @@ window.__ModuleLoader__.load({
8697
9920
  exports.name = "dsh-quick-toc"; // runner attributes slot entries to this registrant
8698
9921
  exports.apply = apply;
8699
9922
  exports.inject = inject;
9923
+ // Which build the page actually loaded. A stale client bundle looks exactly
9924
+ // like a broken feature, so this one string settles it in a single look:
9925
+ // type __dqtBuild in the console.
9926
+ try { if (typeof window !== "undefined") window.__dqtBuild = "0.10.0-glass-card"; } catch (e) {}
8700
9927
 
8701
9928
  // inject the small keyframe + panel-scrollbar stylesheet once
8702
9929
  (function () {
@@ -8716,7 +9943,9 @@ window.__ModuleLoader__.load({
8716
9943
  // card that is ADOPTED from the hover card (it is already on screen, so it must
8717
9944
  // not re-play the entrance and blink).
8718
9945
  ".dqt-hover{animation:dqt-fade-in 0.12s linear both}" +
8719
- ".dqt-hover-static{animation:none}" +
9946
+ // the bookmark view is a different list riding on top of the outline: it eases
9947
+ // in rather than appearing between two frames
9948
+ ".dqt-marks-overlay{animation:dqt-fade-in 0.16s linear both}" + ".dqt-hover-static{animation:none}" +
8720
9949
  ".dqt-hover-closing{animation:dqt-fade-out 0.16s cubic-bezier(0.2, 0.9, 0.3, 1) both;pointer-events:none}" +
8721
9950
  // the outline row menu: same vocabulary as the level picker (a small pop)
8722
9951
  ".dqt-rowmenu{animation:dqt-pop-in 0.14s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
@@ -8734,9 +9963,25 @@ window.__ModuleLoader__.load({
8734
9963
  // on the way out — the same vocabulary as the docked panel sliding in from its
8735
9964
  // edge. The clip box it lives in (see the portal) is what keeps it from being
8736
9965
  // seen above that line, so it reads as emerging from under the view tabs.
8737
- "@keyframes dqt-sheet-in{from{transform:translateY(-100%)}to{transform:translateY(0)}}" +
8738
- "@keyframes dqt-sheet-out{from{transform:translateY(0)}to{transform:translateY(-100%)}}" +
8739
- ".dqt-sheet-open{animation:dqt-sheet-in 0.46s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform}" +
9966
+ // K: the curtain drops a hair past its resting line and settles back, so it
9967
+ // reads as a sheet landing rather than a panel arriving at a coordinate.
9968
+ // The overshoot is small and late (88%) so the settle is a long, soft
9969
+ // exhale — at 70%/6px it read as a hard snap.
9970
+ // K: the curtain drops a hair past its resting line and settles back.
9971
+ // The settle gets its OWN timing function and a long segment (62% -> 100%):
9972
+ // with the overshoot at 88% the return lasted 55ms and, because the drop's
9973
+ // ease-out is fast at a segment's start, it read as a snap back into place.
9974
+ // At the turning point the velocity is zero, so that leg eases IN as well as
9975
+ // out — that is what makes it a settle instead of a bounce.
9976
+ "@keyframes dqt-sheet-in{from{transform:translateY(calc(-100% - 24px));animation-timing-function:cubic-bezier(0.22, 0.9, 0.3, 1)}62%{transform:translateY(4px);animation-timing-function:cubic-bezier(0.45, 0, 0.25, 1)}to{transform:translateY(0)}}" +
9977
+ // The retract has to clear the shadow too, not just the sheet: the sheet is
9978
+ // 24px shorter than its shell (that strip is where the drop shadow lands),
9979
+ // so `translateY(-100%)` parked its own shadow exactly at the shell's top
9980
+ // edge and the state flip 40ms later blinked that band away.
9981
+ "@keyframes dqt-sheet-out{from{transform:translateY(0)}to{transform:translateY(calc(-100% - 24px))}}" +
9982
+ // 0.5s: the drop is 62% of it and the settle the rest, so the settle is a
9983
+ // readable ~190ms rather than a two-frame correction
9984
+ ".dqt-sheet-open{animation:dqt-sheet-in 0.5s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform}" +
8740
9985
  ".dqt-sheet-closing{animation:dqt-sheet-out 0.3s cubic-bezier(0.4, 0, 1, 1) both;pointer-events:none;will-change:transform}" +
8741
9986
  // The curtain's own drop and retract are deliberately NOT gated on
8742
9987
  // `prefers-reduced-motion`: with the host system's animations switched off (Windows