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/CHANGELOG.en.md +30 -0
- package/CHANGELOG.md +30 -0
- package/README.en.md +3 -2
- package/README.md +3 -2
- package/icon.svg +14 -0
- package/lib/client.js +1333 -88
- package/lib/index.js +21 -0
- package/package.json +6 -2
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
|
-
|
|
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,
|
|
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(
|
|
1912
|
-
mark.style.boxShadow = "0 0 0 1px rgba(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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) -
|
|
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 +
|
|
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) >
|
|
5615
|
+
if (mine.done || (ticks += 1) > 3) { clearInterval(iv); return; }
|
|
5096
5616
|
var next = getTarget(sp);
|
|
5097
|
-
|
|
5098
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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" ?
|
|
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 ?
|
|
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
|
-
|
|
6505
|
-
|
|
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(
|
|
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:
|
|
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
|
-
//
|
|
6546
|
-
//
|
|
6547
|
-
//
|
|
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
|
-
|
|
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(
|
|
6558
|
-
: "inset(" + zLen(-SHADOW_BLEED) + " " + zLen(geoOpen ? -SHADOW_BLEED : -SHADOW_REACH) + " " + zLen(-SHADOW_BLEED) + " " + zLen(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6862
|
-
|
|
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:
|
|
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 ?
|
|
7115
|
-
color: opts.pinned ?
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
8015
|
+
borderRadius: 10,
|
|
7163
8016
|
cornerShape: "round",
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
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
|
|
7196
|
-
var
|
|
7197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7887
|
-
//
|
|
7888
|
-
//
|
|
7889
|
-
//
|
|
7890
|
-
//
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
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
|