dsh-quick-toc 0.9.4 → 0.10.0
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 +24 -0
- package/CHANGELOG.md +24 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/icon.svg +14 -0
- package/lib/client.js +1227 -74
- package/lib/index.js +21 -0
- package/package.json +9 -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; }
|
|
@@ -3113,6 +3456,132 @@ window.__ModuleLoader__.load({
|
|
|
3113
3456
|
// two comparisons disagree forever — the panel then republishes its default on
|
|
3114
3457
|
// every pass and stomps a fresh change with the value it had just read back.
|
|
3115
3458
|
var prefs = usePrefs();
|
|
3459
|
+
// ---- the day strip (see its call site) -----------------------------------
|
|
3460
|
+
// Declared up here with the other hooks: an early "nothing to show yet"
|
|
3461
|
+
// return must not skip them, or the first empty render and every real one
|
|
3462
|
+
// disagree on the hook count (React #310). It reads the list's scroll
|
|
3463
|
+
// position directly — the labels are plain DOM and offsetTop is all the
|
|
3464
|
+
// answer needs — so the handler only ever sets one string.
|
|
3465
|
+
var _sDayStrip = react.useState("");
|
|
3466
|
+
var dayStripText = _sDayStrip[0];
|
|
3467
|
+
var setDayStripText = _sDayStrip[1];
|
|
3468
|
+
react.useEffect(function () {
|
|
3469
|
+
var el = listRef.current;
|
|
3470
|
+
if (!el || typeof el.addEventListener !== "function") return undefined;
|
|
3471
|
+
var sync = function () {
|
|
3472
|
+
var top = el.scrollTop || 0;
|
|
3473
|
+
if (top < 2) { setDayStripText(""); return; }
|
|
3474
|
+
var labels = typeof el.querySelectorAll === "function"
|
|
3475
|
+
? el.querySelectorAll("[data-dqt-grouplabel]") : [];
|
|
3476
|
+
var cur = null;
|
|
3477
|
+
for (var i = 0; i < labels.length; i++) {
|
|
3478
|
+
if ((labels[i].offsetTop || 0) - 6 <= top) cur = labels[i];
|
|
3479
|
+
else break;
|
|
3480
|
+
}
|
|
3481
|
+
setDayStripText(cur ? String(cur.getAttribute("data-dqt-day") || "") : "");
|
|
3482
|
+
};
|
|
3483
|
+
el.addEventListener("scroll", sync);
|
|
3484
|
+
sync();
|
|
3485
|
+
return function () { el.removeEventListener("scroll", sync); };
|
|
3486
|
+
}, []);
|
|
3487
|
+
// ---- the curtain's own height (dragged from its bottom edge) -------------
|
|
3488
|
+
// The panel's blur twin needs the panel's REAL height: the panel's own height
|
|
3489
|
+
// is often auto (a stored 0), and a twin with no height blurs nothing — that
|
|
3490
|
+
// is what made the first twin invisible.
|
|
3491
|
+
//
|
|
3492
|
+
// Measured through a ResizeObserver, NOT on every render: reading offsetHeight
|
|
3493
|
+
// in a layout effect forces a synchronous layout of the whole (often huge)
|
|
3494
|
+
// outline on every single render, which is exactly the kind of thing that
|
|
3495
|
+
// turns a scroll or a jump into visible jank.
|
|
3496
|
+
var _sBlurH = react.useState(0);
|
|
3497
|
+
var panelBlurH = _sBlurH[0];
|
|
3498
|
+
var setPanelBlurH = _sBlurH[1];
|
|
3499
|
+
react.useEffect(function () {
|
|
3500
|
+
var el = panelRootRef.current;
|
|
3501
|
+
if (!el) return undefined;
|
|
3502
|
+
var measure = function () {
|
|
3503
|
+
var h = el.offsetHeight ? Math.round(el.offsetHeight) : 0;
|
|
3504
|
+
setPanelBlurH(function (prev) { return prev === h ? prev : h; });
|
|
3505
|
+
};
|
|
3506
|
+
if (typeof ResizeObserver === "function") {
|
|
3507
|
+
var ro = new ResizeObserver(measure);
|
|
3508
|
+
ro.observe(el);
|
|
3509
|
+
measure();
|
|
3510
|
+
return function () { ro.disconnect(); };
|
|
3511
|
+
}
|
|
3512
|
+
measure();
|
|
3513
|
+
return undefined;
|
|
3514
|
+
}, [open, sheetOpen]);
|
|
3515
|
+
var _sAutoGlass = react.useState(autoGlassOff);
|
|
3516
|
+
var autoGlassState = _sAutoGlass[0];
|
|
3517
|
+
var setAutoGlassOff = _sAutoGlass[1];
|
|
3518
|
+
var _sSheetFrac = react.useState(null);
|
|
3519
|
+
var sheetDragFrac = _sSheetFrac[0];
|
|
3520
|
+
var setSheetDragFrac = _sSheetFrac[1];
|
|
3521
|
+
var sheetDragRef = react.useRef(null);
|
|
3522
|
+
var sheetResizeStart = function (e) {
|
|
3523
|
+
if (!e || e.button === 2) return;
|
|
3524
|
+
var h = (sheetRef.current && sheetRef.current.clientHeight) || window.innerHeight || 600;
|
|
3525
|
+
sheetDragRef.current = { y: e.clientY, frac: prefs.sheetHeight, h: h };
|
|
3526
|
+
setSheetDragFrac(prefs.sheetHeight);
|
|
3527
|
+
try { if (e.currentTarget && e.currentTarget.setPointerCapture) e.currentTarget.setPointerCapture(e.pointerId); } catch (err) {}
|
|
3528
|
+
};
|
|
3529
|
+
var sheetResizeMove = function (e) {
|
|
3530
|
+
var d = sheetDragRef.current;
|
|
3531
|
+
if (!d) return;
|
|
3532
|
+
// dragging DOWN grows the curtain: the free edge is its bottom
|
|
3533
|
+
setSheetDragFrac(clampFrac(d.frac + (e.clientY - d.y) / d.h, 1));
|
|
3534
|
+
};
|
|
3535
|
+
var sheetResizeEnd = function () {
|
|
3536
|
+
if (!sheetDragRef.current) return;
|
|
3537
|
+
sheetDragRef.current = null;
|
|
3538
|
+
if (sheetDragFrac !== null) prefsStore.set({ sheetHeight: sheetDragFrac });
|
|
3539
|
+
setSheetDragFrac(null);
|
|
3540
|
+
};
|
|
3541
|
+
// ---- slow-device probe (see AUTO_OFF_KEY) --------------------------------
|
|
3542
|
+
// Sampled once per curtain opening, and only while the material is on: the
|
|
3543
|
+
// first 700ms of animated curtain is the heaviest thing this plugin does, so
|
|
3544
|
+
// a p90 frame time over ~26ms there means the blur is costing more than the
|
|
3545
|
+
// look is worth on this machine.
|
|
3546
|
+
react.useEffect(function () {
|
|
3547
|
+
if (!sheetOpen || !prefs.glass || autoGlassOff) return undefined;
|
|
3548
|
+
if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") return undefined;
|
|
3549
|
+
var frames = [];
|
|
3550
|
+
var last = 0;
|
|
3551
|
+
var stop = false;
|
|
3552
|
+
var tick = function (t) {
|
|
3553
|
+
if (stop) return;
|
|
3554
|
+
if (last) frames.push(t - last);
|
|
3555
|
+
last = t;
|
|
3556
|
+
if (frames.length < 40) window.requestAnimationFrame(tick);
|
|
3557
|
+
else finish();
|
|
3558
|
+
};
|
|
3559
|
+
var finish = function () {
|
|
3560
|
+
stop = true;
|
|
3561
|
+
if (!frames.length) return;
|
|
3562
|
+
var sorted = frames.slice().sort(function (a, b) { return a - b; });
|
|
3563
|
+
var p90 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.9))];
|
|
3564
|
+
if (p90 > 26) {
|
|
3565
|
+
autoGlassOff = true;
|
|
3566
|
+
try { localStorage.setItem(AUTO_OFF_KEY, "1"); } catch (e) {}
|
|
3567
|
+
setAutoGlassOff(true);
|
|
3568
|
+
}
|
|
3569
|
+
};
|
|
3570
|
+
var id = window.requestAnimationFrame(tick);
|
|
3571
|
+
var guard = setTimeout(finish, 1200);
|
|
3572
|
+
return function () { stop = true; clearTimeout(guard); try { window.cancelAnimationFrame(id); } catch (e) {} };
|
|
3573
|
+
}, [sheetOpen]);
|
|
3574
|
+
// The render's own palette: blue shares the module object itself (default
|
|
3575
|
+
// path: zero behaviour change), any other theme merges the same keys.
|
|
3576
|
+
// Every C.* use below this line — rows, chips, the accent bar, hit marks —
|
|
3577
|
+
// follows the preference without touching a single call site.
|
|
3578
|
+
var C = glassC(themeC(prefs.accent), {
|
|
3579
|
+
glass: prefs.glass && !autoGlassState,
|
|
3580
|
+
glassStrength: prefs.glassStrength
|
|
3581
|
+
});
|
|
3582
|
+
// the once-injected stylesheet cannot see per-render values: repaint its
|
|
3583
|
+
// three theme-dependent rules whenever the theme colour changes
|
|
3584
|
+
react.useEffect(function () { applyAccentCss(prefs.accent); }, [prefs.accent]);
|
|
3116
3585
|
// Resolve the language for this render FIRST: the group builder below bakes the
|
|
3117
3586
|
// localised turn stamps (昨天 versus a numeric date) into its memo, and `langKey`
|
|
3118
3587
|
// is what tells that memo a language switch has to rebuild it.
|
|
@@ -5091,11 +5560,23 @@ window.__ModuleLoader__.load({
|
|
|
5091
5560
|
landed = true;
|
|
5092
5561
|
if (glide === mine) glide = null;
|
|
5093
5562
|
var ticks = 0;
|
|
5563
|
+
var lastSign = 0;
|
|
5094
5564
|
var iv = setInterval(function () {
|
|
5095
|
-
if (mine.done || (ticks += 1) >
|
|
5565
|
+
if (mine.done || (ticks += 1) > 3) { clearInterval(iv); return; }
|
|
5096
5566
|
var next = getTarget(sp);
|
|
5097
|
-
|
|
5098
|
-
|
|
5567
|
+
var delta = next - sp.scrollTop;
|
|
5568
|
+
// Only ever nudge a SMALL drift, and never twice in a row in opposite
|
|
5569
|
+
// directions. The re-check exists to absorb the last few pixels while
|
|
5570
|
+
// the host finishes measuring; if the measurement itself swings (a
|
|
5571
|
+
// virtualized turn settling under the target) an unbounded correction
|
|
5572
|
+
// yanks the reader away from where they just landed and then back —
|
|
5573
|
+
// "it arrives and flashes back". A jump that really moved is a fresh
|
|
5574
|
+
// glide, not this one's business.
|
|
5575
|
+
if (Math.abs(delta) > 60) { clearInterval(iv); return; }
|
|
5576
|
+
var sign = delta > 0 ? 1 : (delta < 0 ? -1 : 0);
|
|
5577
|
+
if (sign !== 0 && lastSign !== 0 && sign !== lastSign) { clearInterval(iv); return; }
|
|
5578
|
+
if (Math.abs(delta) > 8) { sp.scrollTop = next; lastSign = sign; }
|
|
5579
|
+
}, 140);
|
|
5099
5580
|
};
|
|
5100
5581
|
|
|
5101
5582
|
// fallback: draw the glide ourselves, in real time, decelerating to a stop
|
|
@@ -5289,7 +5770,7 @@ window.__ModuleLoader__.load({
|
|
|
5289
5770
|
// a jump that started from a search hit keeps the hit highlighted
|
|
5290
5771
|
if (query.trim()) {
|
|
5291
5772
|
clearHighlights();
|
|
5292
|
-
highlightRow(row, query.trim(), 0, fuzzy);
|
|
5773
|
+
highlightRow(row, query.trim(), 0, fuzzy, C.accentRgb);
|
|
5293
5774
|
}
|
|
5294
5775
|
return;
|
|
5295
5776
|
}
|
|
@@ -5481,7 +5962,7 @@ window.__ModuleLoader__.load({
|
|
|
5481
5962
|
// occurrence index within the target message (consecutive in matches)
|
|
5482
5963
|
var occ = 0;
|
|
5483
5964
|
for (var p = i - 1; p >= 0 && matches[p].key === m.key; p--) occ++;
|
|
5484
|
-
highlightRow(r, q, occ, fuzzy);
|
|
5965
|
+
highlightRow(r, q, occ, fuzzy, C.accentRgb);
|
|
5485
5966
|
var markEl = r.querySelector(".dqt-current");
|
|
5486
5967
|
var sp = r.closest ? r.closest("[data-conversation-scroll]") : null;
|
|
5487
5968
|
if (sp && markEl) {
|
|
@@ -5540,7 +6021,7 @@ window.__ModuleLoader__.load({
|
|
|
5540
6021
|
title: tip,
|
|
5541
6022
|
onMouseEnter: function (e) {
|
|
5542
6023
|
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
5543
|
-
e.currentTarget.style.color =
|
|
6024
|
+
e.currentTarget.style.color = C.accent;
|
|
5544
6025
|
},
|
|
5545
6026
|
onMouseLeave: function (e) {
|
|
5546
6027
|
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
@@ -5565,6 +6046,10 @@ window.__ModuleLoader__.load({
|
|
|
5565
6046
|
onOpenTurn: openTurn,
|
|
5566
6047
|
hoverStart: hoverStart,
|
|
5567
6048
|
hoverEnd: hoverEnd,
|
|
6049
|
+
// B: which rows carry a bookmark (the flag layer reads this)
|
|
6050
|
+
isMarked: isMarked,
|
|
6051
|
+
// B: the ribbon's colour, resolved here to a concrete hex (see renderItem)
|
|
6052
|
+
markColor: (ACCENTS[normAccent(prefs.accent)] || ACCENTS.blue).dot,
|
|
5568
6053
|
// right-click on a row: pin its preview / jump to the end of its section
|
|
5569
6054
|
rowMenu: openRowMenu,
|
|
5570
6055
|
// the curtain is wide: rows switch to a one-line, multi-part layout that
|
|
@@ -5807,7 +6292,11 @@ window.__ModuleLoader__.load({
|
|
|
5807
6292
|
// ramp over the slide only made the two directions read different lines.
|
|
5808
6293
|
// While parked the wipe covers the box entirely, so the resting opacity can
|
|
5809
6294
|
// simply stay up.
|
|
5810
|
-
|
|
6295
|
+
// Resting opacity used to be 0.72 so the panel stepped out of the way. The
|
|
6296
|
+
// surface is genuinely translucent now (74 % + blur), and dimming on top of
|
|
6297
|
+
// that would leave the labels at ~53 % — text has to stay crisp, so the
|
|
6298
|
+
// "inactive" hint is a much smaller step.
|
|
6299
|
+
var panelOpacity = chatViewActive ? (hovered ? 1 : 0.92) : 0;
|
|
5811
6300
|
|
|
5812
6301
|
// ---- toolbar buttons -----------------------------------------------------
|
|
5813
6302
|
// ONE instance of each, placed either in the docked header row or in the
|
|
@@ -5845,7 +6334,7 @@ window.__ModuleLoader__.load({
|
|
|
5845
6334
|
onMouseEnter: function (e) {
|
|
5846
6335
|
if (disabled) return;
|
|
5847
6336
|
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
5848
|
-
e.currentTarget.style.color =
|
|
6337
|
+
e.currentTarget.style.color = C.accent;
|
|
5849
6338
|
},
|
|
5850
6339
|
onMouseLeave: function (e) {
|
|
5851
6340
|
e.currentTarget.style.background = active ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
@@ -5963,6 +6452,53 @@ window.__ModuleLoader__.load({
|
|
|
5963
6452
|
// is what makes it read centred (the old character needed the same, plus a y nudge for
|
|
5964
6453
|
// its line box, which a path does not have)
|
|
5965
6454
|
var dockBtnEl = iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockTriEl, 12, dockRight ? { x: -1, y: 0 } : { x: 1, y: 0 });
|
|
6455
|
+
// B: the bookmark button. Always present, between the two left-hand controls,
|
|
6456
|
+
// and it is the way in and out of the bookmark view — a control that appeared
|
|
6457
|
+
// and vanished with the count left the reader looking for it.
|
|
6458
|
+
//
|
|
6459
|
+
// The star is the whole face: no number inside it (there is no room in a
|
|
6460
|
+
// twenty-pixel star, and a squeezed digit is worse than none). The count lives
|
|
6461
|
+
// in the tooltip a moment later, where it has a line of its own.
|
|
6462
|
+
var markCount = marks.length > 99 ? "99+" : String(marks.length);
|
|
6463
|
+
var marksTip = onlyMarks
|
|
6464
|
+
? Tp("marks.tip.all", { n: markCount })
|
|
6465
|
+
: Tp("marks.tip.only", { n: markCount });
|
|
6466
|
+
var marksBtnEl = react_jsx_runtime.jsx("button", {
|
|
6467
|
+
onClick: toggleOnlyMarks,
|
|
6468
|
+
title: marksTip,
|
|
6469
|
+
"aria-label": marksTip,
|
|
6470
|
+
"data-dqt-marks": onlyMarks ? "only" : "all",
|
|
6471
|
+
style: {
|
|
6472
|
+
width: 24,
|
|
6473
|
+
height: 24,
|
|
6474
|
+
display: "flex",
|
|
6475
|
+
alignItems: "center",
|
|
6476
|
+
justifyContent: "center",
|
|
6477
|
+
borderRadius: "50%",
|
|
6478
|
+
cornerShape: "round",
|
|
6479
|
+
background: onlyMarks ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
6480
|
+
border: "none",
|
|
6481
|
+
color: C.muted,
|
|
6482
|
+
cursor: "pointer",
|
|
6483
|
+
flex: "none",
|
|
6484
|
+
padding: 0,
|
|
6485
|
+
transition: "background 0.18s ease, color 0.18s ease"
|
|
6486
|
+
},
|
|
6487
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
6488
|
+
key: "star",
|
|
6489
|
+
width: 15, height: 15, viewBox: "0 0 24 24",
|
|
6490
|
+
style: { display: "block" },
|
|
6491
|
+
children: react_jsx_runtime.jsx("path", {
|
|
6492
|
+
// rounded joins (a stroke in the same colour as the fill) soften the
|
|
6493
|
+
// five points; without it the star reads as a spiky asterisk at this size
|
|
6494
|
+
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",
|
|
6495
|
+
fill: "currentColor",
|
|
6496
|
+
stroke: "currentColor",
|
|
6497
|
+
strokeWidth: 2.4,
|
|
6498
|
+
strokeLinejoin: "round"
|
|
6499
|
+
})
|
|
6500
|
+
})
|
|
6501
|
+
}, "marks-btn");
|
|
5966
6502
|
// the curtain keeps its own entry point: the water-drop handle on the top
|
|
5967
6503
|
// edge (sheet.openHandle). The toolbar's curtain button is gone — the header
|
|
5968
6504
|
// is four controls now.
|
|
@@ -6063,7 +6599,7 @@ window.__ModuleLoader__.load({
|
|
|
6063
6599
|
// handle.
|
|
6064
6600
|
react_jsx_runtime.jsx("div", {
|
|
6065
6601
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
6066
|
-
children: [settingsBtnEl, dockBtnEl]
|
|
6602
|
+
children: [settingsBtnEl, marksBtnEl, dockBtnEl]
|
|
6067
6603
|
}),
|
|
6068
6604
|
// spacer: nothing here, it just pins the two pairs to the two ends —
|
|
6069
6605
|
// and it is the first thing to give way when the panel narrows
|
|
@@ -6318,7 +6854,7 @@ window.__ModuleLoader__.load({
|
|
|
6318
6854
|
borderRadius: "50%",
|
|
6319
6855
|
cornerShape: "round",
|
|
6320
6856
|
fontSize: sheetOpen ? 13 : 11,
|
|
6321
|
-
color: searchScope !== "title" ?
|
|
6857
|
+
color: searchScope !== "title" ? C.accent : C.muted,
|
|
6322
6858
|
background: searchScope !== "title" ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
6323
6859
|
border: "none",
|
|
6324
6860
|
cursor: "pointer",
|
|
@@ -6360,7 +6896,7 @@ window.__ModuleLoader__.load({
|
|
|
6360
6896
|
borderRadius: "50%",
|
|
6361
6897
|
cornerShape: "round",
|
|
6362
6898
|
fontSize: sheetOpen ? 13 : 11,
|
|
6363
|
-
color: fuzzy ?
|
|
6899
|
+
color: fuzzy ? C.accent : C.muted,
|
|
6364
6900
|
background: fuzzy ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
6365
6901
|
border: "none",
|
|
6366
6902
|
cursor: searchScope === "cross" ? "default" : "pointer",
|
|
@@ -6405,14 +6941,136 @@ window.__ModuleLoader__.load({
|
|
|
6405
6941
|
},
|
|
6406
6942
|
children: hint === "oldest" ? T("search.hintOldest") : T("search.hintMore")
|
|
6407
6943
|
}) : null;
|
|
6944
|
+
// B: "only bookmarks". The rows come from the stored entries rather than from
|
|
6945
|
+
// the outline tree — a bookmark may point at a turn that is not loaded right
|
|
6946
|
+
// now, and it still has to be listed (it is a place the reader saved).
|
|
6947
|
+
//
|
|
6948
|
+
// These are DELIBERATELY plain rows, not the outline's own row renderer. That
|
|
6949
|
+
// renderer registers data-jump-key / data-nav-row and wires the hover card,
|
|
6950
|
+
// the end control and the preview pinning; reusing it here put a second set of
|
|
6951
|
+
// rows into the DOM carrying the SAME keys as the live outline, which is a
|
|
6952
|
+
// collision waiting to happen for anything that queries rows by key (the jump
|
|
6953
|
+
// target lookup, the keyboard cursor, the follow). A saved place needs a label
|
|
6954
|
+
// and a click — nothing else.
|
|
6955
|
+
var markRowsEl = null;
|
|
6956
|
+
if (onlyMarks && !resultsEl) {
|
|
6957
|
+
// `dense` is a local of the row renderers, not of this component: read the
|
|
6958
|
+
// preference here instead (referencing the other one threw and took the
|
|
6959
|
+
// whole panel down with it).
|
|
6960
|
+
var markDense = prefs.density === "compact";
|
|
6961
|
+
markRowsEl = marks.length ? react_jsx_runtime.jsx("div", {
|
|
6962
|
+
key: "marks",
|
|
6963
|
+
style: { padding: "2px 0 8px" },
|
|
6964
|
+
children: marks.map(function (e, i) {
|
|
6965
|
+
var lvl = Math.min(6, Math.max(1, e.level || 1));
|
|
6966
|
+
return react_jsx_runtime.jsx("div", {
|
|
6967
|
+
"data-dqt-markrow": "1",
|
|
6968
|
+
onClick: function () { jump(e.key, e.idx); },
|
|
6969
|
+
title: e.title || "",
|
|
6970
|
+
style: {
|
|
6971
|
+
display: "flex", alignItems: "center", gap: 6,
|
|
6972
|
+
padding: markDense ? "2px 8px 2px 7px" : "4px 8px 4px 7px",
|
|
6973
|
+
borderRadius: 6, cursor: "pointer",
|
|
6974
|
+
fontSize: 12.5, color: C.text,
|
|
6975
|
+
transition: "background-color 0.15s ease"
|
|
6976
|
+
},
|
|
6977
|
+
onMouseEnter: function (ev) { ev.currentTarget.style.backgroundColor = C.hover; },
|
|
6978
|
+
onMouseLeave: function (ev) { ev.currentTarget.style.backgroundColor = "transparent"; },
|
|
6979
|
+
children: [
|
|
6980
|
+
react_jsx_runtime.jsx("span", {
|
|
6981
|
+
key: "f", style: { color: C.accent, display: "flex", flex: "none" },
|
|
6982
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
6983
|
+
width: 7, height: 9, viewBox: "0 0 7 9", fill: "currentColor",
|
|
6984
|
+
style: { display: "block" },
|
|
6985
|
+
children: react_jsx_runtime.jsx("path", { d: "M0 0h5.6L4.1 2.2l1.5 2.2H1.1V9H0z" })
|
|
6986
|
+
})
|
|
6987
|
+
}),
|
|
6988
|
+
react_jsx_runtime.jsx("span", {
|
|
6989
|
+
key: "l", style: { flex: "none", fontSize: 10, color: C.muted, width: 16 },
|
|
6990
|
+
children: "H" + lvl
|
|
6991
|
+
}),
|
|
6992
|
+
react_jsx_runtime.jsx("span", {
|
|
6993
|
+
key: "t", style: { flex: "1 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
6994
|
+
children: e.title || ""
|
|
6995
|
+
})
|
|
6996
|
+
]
|
|
6997
|
+
}, "mark-row-" + i);
|
|
6998
|
+
})
|
|
6999
|
+
}) : react_jsx_runtime.jsx("div", {
|
|
7000
|
+
key: "marks-empty",
|
|
7001
|
+
style: {
|
|
7002
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
7003
|
+
padding: "30px 16px", color: C.muted, fontSize: 11.5, lineHeight: "17px",
|
|
7004
|
+
textAlign: "center"
|
|
7005
|
+
},
|
|
7006
|
+
children: T("marks.empty")
|
|
7007
|
+
});
|
|
7008
|
+
}
|
|
6408
7009
|
var outlineEl = renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup,
|
|
6409
7010
|
questionsOnly ? Object.assign({}, renderExtra, { questionsOnly: true }) : renderExtra);
|
|
7011
|
+
// M: an empty outline used to be a blank box, which reads as "this is broken"
|
|
7012
|
+
// rather than "there is nothing here yet". One quiet line plus a faded glyph
|
|
7013
|
+
// says which it is. A turn with no headings still becomes a group, so this
|
|
7014
|
+
// only appears for a conversation that has nothing to outline AT ALL.
|
|
7015
|
+
var emptyEl = !resultsEl && shownGroups.length === 0 ? react_jsx_runtime.jsx("div", {
|
|
7016
|
+
key: "empty",
|
|
7017
|
+
style: {
|
|
7018
|
+
display: "flex", flexDirection: "column", alignItems: "center",
|
|
7019
|
+
justifyContent: "center", gap: 8, padding: "34px 18px",
|
|
7020
|
+
color: C.muted, textAlign: "center"
|
|
7021
|
+
},
|
|
7022
|
+
children: [
|
|
7023
|
+
react_jsx_runtime.jsx("svg", {
|
|
7024
|
+
key: "e-ico", width: 26, height: 26, viewBox: "0 0 24 24", fill: "none",
|
|
7025
|
+
stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round",
|
|
7026
|
+
style: { opacity: 0.5, display: "block" },
|
|
7027
|
+
children: [
|
|
7028
|
+
react_jsx_runtime.jsx("line", { key: "a", x1: 4, y1: 6, x2: 20, y2: 6 }),
|
|
7029
|
+
react_jsx_runtime.jsx("line", { key: "b", x1: 4, y1: 12, x2: 15, y2: 12 }),
|
|
7030
|
+
react_jsx_runtime.jsx("line", { key: "c", x1: 4, y1: 18, x2: 11, y2: 18 })
|
|
7031
|
+
]
|
|
7032
|
+
}),
|
|
7033
|
+
react_jsx_runtime.jsx("div", {
|
|
7034
|
+
key: "e-txt",
|
|
7035
|
+
style: { fontSize: 11.5, lineHeight: "17px", opacity: 0.85, maxWidth: 210 },
|
|
7036
|
+
children: T("outline.empty")
|
|
7037
|
+
})
|
|
7038
|
+
]
|
|
7039
|
+
}) : null;
|
|
6410
7040
|
// The curtain keeps the outline in the middle of the surface (its own column,
|
|
6411
7041
|
// centred with room to spare at both sides) and pushes the search in from the
|
|
6412
7042
|
// right; the docked panel keeps its original "results replace the list".
|
|
7043
|
+
// The bookmark view rides ON TOP of the outline instead of replacing it, and
|
|
7044
|
+
// the outline is only hidden (visibility, not display): it keeps its height,
|
|
7045
|
+
// so the scroller keeps its offset and closing the view lands the reader
|
|
7046
|
+
// exactly where they were. Replacing the content clamped the scroll to the top
|
|
7047
|
+
// the moment the shorter list rendered, and putting the offset back afterwards
|
|
7048
|
+
// was a race the browser usually won.
|
|
7049
|
+
var marksOverlayEl = null;
|
|
7050
|
+
if (onlyMarks && !resultsEl) {
|
|
7051
|
+
marksOverlayEl = react_jsx_runtime.jsx("div", {
|
|
7052
|
+
className: "dqt-marks-overlay",
|
|
7053
|
+
style: {
|
|
7054
|
+
position: "absolute",
|
|
7055
|
+
left: 0,
|
|
7056
|
+
// the curtain's search column keeps its own strip: the overlay covers the
|
|
7057
|
+
// outline, not the hits
|
|
7058
|
+
right: sheetOpen && sheetColW > 0 ? sheetColW : 0,
|
|
7059
|
+
top: 0,
|
|
7060
|
+
bottom: 0,
|
|
7061
|
+
overflowY: "auto",
|
|
7062
|
+
overflowAnchor: "none",
|
|
7063
|
+
padding: sheetOpen ? "0 20px 8px" : "0 8px 6px",
|
|
7064
|
+
direction: (sheetOpen || dockRight) ? "ltr" : "rtl"
|
|
7065
|
+
},
|
|
7066
|
+
children: react_jsx_runtime.jsx("div", { style: { direction: "ltr" }, children: markRowsEl })
|
|
7067
|
+
});
|
|
7068
|
+
}
|
|
7069
|
+
// across the curtain the results live in their own column, so the outline stays
|
|
7070
|
+
// where it is; only the docked panel swaps the list for the hits
|
|
6413
7071
|
var listChildren = sheetOpen
|
|
6414
|
-
? [outlineEl, hintEl]
|
|
6415
|
-
: (resultsEl ? [resultsEl, hintEl] : outlineEl);
|
|
7072
|
+
? [outlineEl || emptyEl, hintEl]
|
|
7073
|
+
: (resultsEl ? [resultsEl, hintEl] : (outlineEl || emptyEl));
|
|
6416
7074
|
var listEl = react_jsx_runtime.jsx("div", {
|
|
6417
7075
|
ref: listRef,
|
|
6418
7076
|
className: "dqt-list",
|
|
@@ -6422,6 +7080,9 @@ window.__ModuleLoader__.load({
|
|
|
6422
7080
|
role: "group",
|
|
6423
7081
|
"aria-label": T("a11y.list"),
|
|
6424
7082
|
style: {
|
|
7083
|
+
// hidden rather than unmounted while the bookmark view is up (see above):
|
|
7084
|
+
// it keeps its height and therefore its scroll offset
|
|
7085
|
+
visibility: onlyMarks && !resultsEl ? "hidden" : undefined,
|
|
6425
7086
|
overflowY: "auto",
|
|
6426
7087
|
// We are the only thing that holds the reader's place in this list (see the
|
|
6427
7088
|
// paging block): the browser's own scroll anchoring would also adjust
|
|
@@ -6434,6 +7095,11 @@ window.__ModuleLoader__.load({
|
|
|
6434
7095
|
// header that many pixels below the toolbar (a visible gap). Spacing
|
|
6435
7096
|
// between groups comes from the per-group divider instead.
|
|
6436
7097
|
padding: sheetOpen ? "0 20px 8px" : "0 8px 6px",
|
|
7098
|
+
// the rows dissolve at BOTH edges instead of being cut by a hard line. The
|
|
7099
|
+
// top fade is safe now that the day strip lives outside the scroller: it
|
|
7100
|
+
// thins the rows approaching the strip, never the label itself.
|
|
7101
|
+
maskImage: "linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 22px), transparent 100%)",
|
|
7102
|
+
WebkitMaskImage: "linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 22px), transparent 100%)",
|
|
6437
7103
|
flex: "1 1 auto",
|
|
6438
7104
|
minHeight: 0,
|
|
6439
7105
|
// across the curtain the list does not touch the edges: it is a centred
|
|
@@ -6499,10 +7165,36 @@ window.__ModuleLoader__.load({
|
|
|
6499
7165
|
var showBack = awayFromRestore(listRef.current);
|
|
6500
7166
|
// the outline and the search column share one row in BOTH layouts, so the list
|
|
6501
7167
|
// is never moved between parents when the curtain opens or closes
|
|
7168
|
+
var dayStripEl = react_jsx_runtime.jsx("div", {
|
|
7169
|
+
"data-dqt-daystrip": "1",
|
|
7170
|
+
style: {
|
|
7171
|
+
flex: "none",
|
|
7172
|
+
// the space is reserved even while empty, so the list never jumps when
|
|
7173
|
+
// the first label scrolls under the edge. A STRING on purpose: the group
|
|
7174
|
+
// header's own height is the number 18, and several checks use that to
|
|
7175
|
+
// find header rows — the strip must not be mistaken for one.
|
|
7176
|
+
height: "18px",
|
|
7177
|
+
display: "flex",
|
|
7178
|
+
alignItems: "center",
|
|
7179
|
+
padding: sheetOpen ? "0 20px" : "0 10px",
|
|
7180
|
+
fontSize: 11,
|
|
7181
|
+
lineHeight: "14px",
|
|
7182
|
+
color: C.muted,
|
|
7183
|
+
whiteSpace: "nowrap",
|
|
7184
|
+
overflow: "hidden",
|
|
7185
|
+
textOverflow: "ellipsis",
|
|
7186
|
+
pointerEvents: "none"
|
|
7187
|
+
},
|
|
7188
|
+
// the bookmark view is a different list with its own scroll: the day label
|
|
7189
|
+
// would go on showing the last day the outline was scrolled to
|
|
7190
|
+
children: onlyMarks ? "" : dayStripText
|
|
7191
|
+
});
|
|
6502
7192
|
var bodyRowEl = react_jsx_runtime.jsx("div", {
|
|
6503
7193
|
className: "dqt-body-row",
|
|
6504
|
-
|
|
6505
|
-
|
|
7194
|
+
// `position: relative` is what lets the bookmark overlay cover the list
|
|
7195
|
+
// without becoming part of its scrolled content
|
|
7196
|
+
style: { position: "relative", flex: "1 1 auto", minHeight: 0, display: "flex", flexDirection: "row", alignItems: "stretch" },
|
|
7197
|
+
children: [listEl, marksOverlayEl, sheetSearchColEl]
|
|
6506
7198
|
});
|
|
6507
7199
|
// one geometry for the panel and its shadow twin, merged (never duplicated)
|
|
6508
7200
|
var mergeStyle = function (base, extra) {
|
|
@@ -6520,7 +7212,7 @@ window.__ModuleLoader__.load({
|
|
|
6520
7212
|
maxHeight: zLen(vMaxH),
|
|
6521
7213
|
height: panelH > 0 ? zLen(panelH) : undefined,
|
|
6522
7214
|
zoom: zoom === 1 ? undefined : zoom,
|
|
6523
|
-
borderRadius:
|
|
7215
|
+
borderRadius: RADIUS_PANEL,
|
|
6524
7216
|
transform: panelTransform,
|
|
6525
7217
|
transition: viewFading ? "opacity 0.3s ease" : panelTransition,
|
|
6526
7218
|
// held back while the edge handle slips home (see edgeLeaving): the panel is
|
|
@@ -6556,6 +7248,51 @@ window.__ModuleLoader__.load({
|
|
|
6556
7248
|
var shadowClip = dockRight
|
|
6557
7249
|
? "inset(" + zLen(-SHADOW_BLEED) + " " + zLen(shadowWipe - SHADOW_REACH) + " " + zLen(-SHADOW_BLEED) + " " + zLen(geoOpen ? -SHADOW_BLEED : SHADOW_REACH) + ")"
|
|
6558
7250
|
: "inset(" + zLen(-SHADOW_BLEED) + " " + zLen(geoOpen ? -SHADOW_BLEED : -SHADOW_REACH) + " " + zLen(-SHADOW_BLEED) + " " + zLen(shadowWipe - SHADOW_REACH) + ")";
|
|
7251
|
+
// The panel's blur lives OUTSIDE the clipping wrapper (see the portal list):
|
|
7252
|
+
// inside it, any backdrop-filter promotes a layer in that subtree and the
|
|
7253
|
+
// panel drops beneath the conversation. As a body-level twin with the same
|
|
7254
|
+
// geometry it samples the page normally, and the panel's own translucent
|
|
7255
|
+
// fill is painted over it.
|
|
7256
|
+
// A body-level blur twin was tried and removed: it composites as a SECOND
|
|
7257
|
+
// pass, so the panel's own translucency showed the page straight through
|
|
7258
|
+
// while the twin showed it blurred — the reader sees "one blurred layer and
|
|
7259
|
+
// one clear layer" stacked. A surface can only read as ONE material when the
|
|
7260
|
+
// blur and the fill are painted by the same element, and that is impossible
|
|
7261
|
+
// for the docked panel where it currently lives (a backdrop-filter anywhere
|
|
7262
|
+
// inside its clipping wrapper drops the panel under the conversation).
|
|
7263
|
+
// The blur twin. It lives as a DIRECT child of the clipping shell — the same
|
|
7264
|
+
// depth as the curtain, which is the only place in this subtree where a
|
|
7265
|
+
// backdrop-filter has been observed to sample the page without dragging the
|
|
7266
|
+
// panel under the conversation. The panel's own fill is painted over it, so
|
|
7267
|
+
// a twin that fails costs the blur only.
|
|
7268
|
+
var panelGlassTwinEl = (!sheetOpen && C.glassOn && panelBlurH > 0)
|
|
7269
|
+
? react_jsx_runtime.jsx("div", {
|
|
7270
|
+
"data-dqt-panel-glass": "1",
|
|
7271
|
+
style: mergeStyle(panelGeoStyle, {
|
|
7272
|
+
zIndex: Z_PANEL - 1,
|
|
7273
|
+
height: zLen(panelBlurH),
|
|
7274
|
+
background: "transparent",
|
|
7275
|
+
backdropFilter: C.panelBlur,
|
|
7276
|
+
WebkitBackdropFilter: C.panelBlur,
|
|
7277
|
+
opacity: panelOpacity,
|
|
7278
|
+
pointerEvents: "none",
|
|
7279
|
+
borderRadius: RADIUS_PANEL,
|
|
7280
|
+
clipPath: panelClip
|
|
7281
|
+
})
|
|
7282
|
+
})
|
|
7283
|
+
: null;
|
|
7284
|
+
// NO live blur on the docked panel itself — settled by experiment, not preference.
|
|
7285
|
+
// Every placement was tried in the real host: on the panel itself, on a
|
|
7286
|
+
// child layer, and on a sibling twin behind it. All three made the panel
|
|
7287
|
+
// disappear, and a child layer inside the panel's own `opacity` group
|
|
7288
|
+
// produced no blur at all (an element with opacity is a backdrop root, so
|
|
7289
|
+
// the filter sampled that element's own content). The panel lives inside a
|
|
7290
|
+
// clipping, fixed wrapper that the host's compositor keeps below the
|
|
7291
|
+
// conversation; anything that promotes a layer in there loses the stack.
|
|
7292
|
+
// So the panel keeps a translucent FILL (which reads as glass on its own:
|
|
7293
|
+
// the rim, the gutter shadow and the see-through body do the work) and the
|
|
7294
|
+
// live blur stays on the curtain, the transient surface — exactly the split
|
|
7295
|
+
// Windows makes between Mica (persistent) and Acrylic (transient).
|
|
6559
7296
|
var panelShadowEl = react_jsx_runtime.jsx("div", {
|
|
6560
7297
|
"data-dqt-panel-shadow": "1",
|
|
6561
7298
|
style: mergeStyle(panelGeoStyle, {
|
|
@@ -6564,9 +7301,14 @@ window.__ModuleLoader__.load({
|
|
|
6564
7301
|
opacity: panelOpacity,
|
|
6565
7302
|
pointerEvents: "none",
|
|
6566
7303
|
clipPath: shadowClip,
|
|
6567
|
-
|
|
7304
|
+
// the halo is the panel's lift; the offset line adds the gutter shadow on
|
|
7305
|
+
// the conversation side, so the sheet reads as hovering over the transcript
|
|
7306
|
+
boxShadow: (isDarkScheme()
|
|
6568
7307
|
? "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)"
|
|
7308
|
+
: "0 0 24px -4px rgba(0, 0, 0, 0.38), 0 0 10px -2px rgba(0, 0, 0, 0.26)")
|
|
7309
|
+
+ (dockRight
|
|
7310
|
+
? ", -12px 0 20px -14px rgba(0, 0, 0, 0.45)"
|
|
7311
|
+
: ", 12px 0 20px -14px rgba(0, 0, 0, 0.45)")
|
|
6570
7312
|
})
|
|
6571
7313
|
});
|
|
6572
7314
|
var panelEl = react_jsx_runtime.jsx("div", {
|
|
@@ -6593,7 +7335,9 @@ window.__ModuleLoader__.load({
|
|
|
6593
7335
|
minHeight: 0,
|
|
6594
7336
|
display: "flex",
|
|
6595
7337
|
flexDirection: "column",
|
|
6596
|
-
|
|
7338
|
+
// transparent on purpose: inside the curtain the CURTAIN is the glass, and
|
|
7339
|
+
// an opaque child would paint over it
|
|
7340
|
+
background: "transparent",
|
|
6597
7341
|
border: "none",
|
|
6598
7342
|
borderRadius: 0,
|
|
6599
7343
|
zIndex: Z_PANEL,
|
|
@@ -6606,12 +7350,17 @@ window.__ModuleLoader__.load({
|
|
|
6606
7350
|
} : mergeStyle(panelGeoStyle, {
|
|
6607
7351
|
display: "flex",
|
|
6608
7352
|
flexDirection: "column",
|
|
6609
|
-
|
|
7353
|
+
// the filler colour is ALWAYS here (glassBg falls back to the opaque
|
|
7354
|
+
// panel colour where blur is unavailable); the blur rides on the child
|
|
7355
|
+
background: C.glassBg,
|
|
6610
7356
|
border: "none",
|
|
6611
7357
|
zIndex: Z_PANEL,
|
|
6612
7358
|
overflow: "hidden",
|
|
6613
7359
|
color: C.text,
|
|
6614
7360
|
opacity: panelOpacity,
|
|
7361
|
+
// makes this element a stacking context so the glass layer's z-index:-1
|
|
7362
|
+
// stays inside it (behind the rows, in front of the page)
|
|
7363
|
+
isolation: "isolate",
|
|
6615
7364
|
clipPath: panelClip,
|
|
6616
7365
|
pointerEvents: chatViewActive ? "auto" : "none"
|
|
6617
7366
|
}),
|
|
@@ -6656,6 +7405,13 @@ window.__ModuleLoader__.load({
|
|
|
6656
7405
|
// the docked panel keeps its search band under the header; across the
|
|
6657
7406
|
// curtain the same row lives at the top of the pushed-in search column
|
|
6658
7407
|
sheetOpen ? null : searchRowEl,
|
|
7408
|
+
// the day strip: the "which day am I in" label, OUTSIDE the scroller.
|
|
7409
|
+
// A sticky label would have to be opaque to hide the rows passing under
|
|
7410
|
+
// it, and an opaque strip on a translucent surface is the band that reads
|
|
7411
|
+
// as a cut. Out here it needs no fill at all — it is just text on the
|
|
7412
|
+
// same material — while still staying put for as long as the reader
|
|
7413
|
+
// scrolls, and the list fades its first rows out before they reach it.
|
|
7414
|
+
dayStripEl,
|
|
6659
7415
|
bodyRowEl,
|
|
6660
7416
|
// "back to the newest row": floats over the list's lower-right corner and
|
|
6661
7417
|
// fades in the moment the newest turns fall below the fold. Kept MOUNTED
|
|
@@ -7058,6 +7814,11 @@ window.__ModuleLoader__.load({
|
|
|
7058
7814
|
// takes pointer events, so its text can be selected and its pin can be clicked);
|
|
7059
7815
|
// the hover card stays click-through except for its pin button.
|
|
7060
7816
|
var renderPreviewCard = function (card, opts) {
|
|
7817
|
+
// the per-render palette (accent + glass) has to be handed in: this body
|
|
7818
|
+
// closed over the module-level C, whose glass fields are undefined, so the
|
|
7819
|
+
// card silently fell back to the opaque colour while everything else was
|
|
7820
|
+
// translucent
|
|
7821
|
+
var P = (opts && opts.palette) || C;
|
|
7061
7822
|
var cardW = 264;
|
|
7062
7823
|
var top = Math.max(8, Math.min(card.top - 4, window.innerHeight - 210));
|
|
7063
7824
|
var left = dockRight
|
|
@@ -7088,7 +7849,7 @@ window.__ModuleLoader__.load({
|
|
|
7088
7849
|
style: { display: "flex", alignItems: "flex-start", gap: 6 },
|
|
7089
7850
|
children: [
|
|
7090
7851
|
react_jsx_runtime.jsx("div", {
|
|
7091
|
-
style: { flex: "1 1 auto", minWidth: 0, fontSize: 12, fontWeight: 600, color:
|
|
7852
|
+
style: { flex: "1 1 auto", minWidth: 0, fontSize: 12, fontWeight: 600, color: P.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
7092
7853
|
children: card.title
|
|
7093
7854
|
}, "hc-t"),
|
|
7094
7855
|
react_jsx_runtime.jsx("button", {
|
|
@@ -7111,8 +7872,8 @@ window.__ModuleLoader__.load({
|
|
|
7111
7872
|
justifyContent: "center",
|
|
7112
7873
|
border: "none",
|
|
7113
7874
|
borderRadius: 6,
|
|
7114
|
-
background: opts.pinned ?
|
|
7115
|
-
color: opts.pinned ?
|
|
7875
|
+
background: opts.pinned ? P.hover : "transparent",
|
|
7876
|
+
color: opts.pinned ? P.text : P.muted,
|
|
7116
7877
|
cursor: "pointer",
|
|
7117
7878
|
// the hover card is click-through except for this button, so reaching
|
|
7118
7879
|
// for it does not depend on the row staying hovered
|
|
@@ -7128,17 +7889,17 @@ window.__ModuleLoader__.load({
|
|
|
7128
7889
|
];
|
|
7129
7890
|
if (card.meta) {
|
|
7130
7891
|
kids.push(react_jsx_runtime.jsx("div", {
|
|
7131
|
-
style: { fontSize: 10, color:
|
|
7892
|
+
style: { fontSize: 10, color: P.muted, opacity: 0.8, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
7132
7893
|
children: card.meta
|
|
7133
7894
|
}, "hc-m"));
|
|
7134
7895
|
}
|
|
7135
7896
|
kids.push(react_jsx_runtime.jsx("div", {
|
|
7136
|
-
style: { fontSize: 11, lineHeight: "16px", color:
|
|
7897
|
+
style: { fontSize: 11, lineHeight: "16px", color: P.text, opacity: 0.9, marginTop: 4, maxHeight: 116, overflow: "hidden" },
|
|
7137
7898
|
children: body || (card.ghost ? T("turn.loadTip") : "")
|
|
7138
7899
|
}, "hc-b"));
|
|
7139
7900
|
if (card.ghost) {
|
|
7140
7901
|
kids.push(react_jsx_runtime.jsx("div", {
|
|
7141
|
-
style: { fontSize: 10, color:
|
|
7902
|
+
style: { fontSize: 10, color: P.muted, opacity: 0.7, marginTop: 4 },
|
|
7142
7903
|
children: T("preview.truncated")
|
|
7143
7904
|
}, "hc-n"));
|
|
7144
7905
|
}
|
|
@@ -7159,12 +7920,18 @@ window.__ModuleLoader__.load({
|
|
|
7159
7920
|
left: left,
|
|
7160
7921
|
width: cardW,
|
|
7161
7922
|
padding: "8px 10px",
|
|
7162
|
-
borderRadius:
|
|
7923
|
+
borderRadius: 10,
|
|
7163
7924
|
cornerShape: "round",
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7925
|
+
// The same material as the panel: the same translucent fill, the same
|
|
7926
|
+
// rim, and the same SMALL radius of blur — the card floats in this
|
|
7927
|
+
// portal (no opacity group, nobody's parent), so its backdrop-filter
|
|
7928
|
+
// samples the page exactly like the curtain's.
|
|
7929
|
+
background: P.glassOn ? P.glassBg : P.panelBg,
|
|
7930
|
+
backdropFilter: P.glassOn ? P.panelBlur : undefined,
|
|
7931
|
+
WebkitBackdropFilter: P.glassOn ? P.panelBlur : undefined,
|
|
7932
|
+
color: P.text,
|
|
7933
|
+
border: "1px solid " + P.panelBorder,
|
|
7934
|
+
boxShadow: P.glassOn ? rimInset() + ", 0 8px 24px rgba(0,0,0,0.28)" : "0 8px 24px rgba(0,0,0,0.28)",
|
|
7168
7935
|
// above the panel and the curtain — it is a sibling of both in this
|
|
7169
7936
|
// portal, and both sit below it in the layer band we claim
|
|
7170
7937
|
zIndex: Z_HOVER + (opts.pinned ? 1 : 0),
|
|
@@ -7182,21 +7949,70 @@ window.__ModuleLoader__.load({
|
|
|
7182
7949
|
// and it only leaves the screen through the same fade the hover card uses.
|
|
7183
7950
|
var pinnedVisible = !!(pinnedCard && open);
|
|
7184
7951
|
var pinnedEl = pinnedVisible
|
|
7185
|
-
? renderPreviewCard(pinnedCard, { pinned: true, closing: pinnedClosing })
|
|
7952
|
+
? renderPreviewCard(pinnedCard, { pinned: true, closing: pinnedClosing, palette: C })
|
|
7186
7953
|
: null;
|
|
7187
7954
|
var hoverEl = null;
|
|
7188
|
-
if (hoverCard && open) hoverEl = renderPreviewCard(hoverCard, { pinned: false, closing: hoverClosing });
|
|
7955
|
+
if (hoverCard && open) hoverEl = renderPreviewCard(hoverCard, { pinned: false, closing: hoverClosing, palette: C });
|
|
7189
7956
|
|
|
7190
7957
|
// ---- the row menu ---------------------------------------------------------
|
|
7191
7958
|
// Small, fixed-position, keyboard-reachable: ↑/↓ move, Enter runs, Esc closes.
|
|
7192
7959
|
var rowMenuEl = null;
|
|
7193
7960
|
if (rowMenu && open) {
|
|
7194
7961
|
var menuW = 208;
|
|
7195
|
-
var
|
|
7196
|
-
var
|
|
7197
|
-
|
|
7962
|
+
var menuLeft;
|
|
7963
|
+
var menuTop;
|
|
7964
|
+
// NOTE: the height (and therefore the clamped top) can only be known once
|
|
7965
|
+
// the item list exists — computing it above the list dereferenced a hoisted
|
|
7966
|
+
// `var` and threw on every open.
|
|
7967
|
+
// Two sources open this menu: an outline row and a time row (the group
|
|
7968
|
+
// header). A time row has no preview to pin and no single heading to copy,
|
|
7969
|
+
// so those two entries are absent there; everything else is identical.
|
|
7970
|
+
var menuIsGroup = !!(rowMenu.info && rowMenu.info.kind === "group");
|
|
7971
|
+
var menuCanCopySection = menuIsGroup
|
|
7972
|
+
? !!(rowMenu.info && rowMenu.info.canCopy)
|
|
7973
|
+
: !!(rowMenu.info && rowMenu.info.hasKids);
|
|
7198
7974
|
var menuItems = [
|
|
7199
7975
|
{
|
|
7976
|
+
id: "goto",
|
|
7977
|
+
label: T("menu.goto"),
|
|
7978
|
+
run: function () {
|
|
7979
|
+
var key = rowMenu.key;
|
|
7980
|
+
var idx = rowMenu.idx;
|
|
7981
|
+
closeRowMenu();
|
|
7982
|
+
jump(key, idx === undefined ? 0 : idx);
|
|
7983
|
+
}
|
|
7984
|
+
},
|
|
7985
|
+
{
|
|
7986
|
+
id: "mark",
|
|
7987
|
+
label: isMarked(rowMenu.key, rowMenu.idx) ? T("menu.unmark") : T("menu.mark"),
|
|
7988
|
+
run: function () {
|
|
7989
|
+
toggleMark(rowMenu.key, rowMenu.idx, rowMenu.info && rowMenu.info.title,
|
|
7990
|
+
menuIsGroup ? 0 : (rowMenu.info && rowMenu.info.level));
|
|
7991
|
+
closeRowMenu();
|
|
7992
|
+
}
|
|
7993
|
+
},
|
|
7994
|
+
{ id: "sep1" },
|
|
7995
|
+
menuIsGroup ? null : {
|
|
7996
|
+
id: "copyTitle",
|
|
7997
|
+
label: copiedId === "copyTitle" ? T("menu.copied") : T("menu.copyTitle"),
|
|
7998
|
+
run: function () {
|
|
7999
|
+
copyText((rowMenu.info && rowMenu.info.title) || "");
|
|
8000
|
+
flashCopied("copyTitle");
|
|
8001
|
+
}
|
|
8002
|
+
},
|
|
8003
|
+
// only when there really is an outline below it, so the entry never
|
|
8004
|
+
// copies the same text the entry above it already copies
|
|
8005
|
+
menuCanCopySection ? {
|
|
8006
|
+
id: "copySection",
|
|
8007
|
+
label: copiedId === "copySection" ? T("menu.copied") : T("menu.copySection"),
|
|
8008
|
+
run: function () {
|
|
8009
|
+
var info = rowMenu.info;
|
|
8010
|
+
copyText(info && typeof info.mdOf === "function" ? info.mdOf() : ((info && info.title) || ""));
|
|
8011
|
+
flashCopied("copySection");
|
|
8012
|
+
}
|
|
8013
|
+
} : null,
|
|
8014
|
+
{ id: "sep2" },
|
|
8015
|
+
menuIsGroup ? null : {
|
|
7200
8016
|
id: "pin",
|
|
7201
8017
|
label: isPinned(rowMenu.key) ? T("menu.unpin") : T("menu.pin"),
|
|
7202
8018
|
run: function () {
|
|
@@ -7226,7 +8042,23 @@ window.__ModuleLoader__.load({
|
|
|
7226
8042
|
if (typeof endJumpRow === "function") endJumpRow(key, idx === undefined ? 0 : idx);
|
|
7227
8043
|
}
|
|
7228
8044
|
}
|
|
7229
|
-
]
|
|
8045
|
+
].filter(function (it, i, arr) {
|
|
8046
|
+
// drop the entries this source does not offer, then collapse the
|
|
8047
|
+
// separators that ended up adjacent or at either end
|
|
8048
|
+
if (!it) return false;
|
|
8049
|
+
if (it.id.indexOf("sep") !== 0) return true;
|
|
8050
|
+
var prev = null;
|
|
8051
|
+
for (var p = i - 1; p >= 0; p--) { if (arr[p]) { prev = arr[p]; break; } }
|
|
8052
|
+
var next = null;
|
|
8053
|
+
for (var q = i + 1; q < arr.length; q++) { if (arr[q]) { next = arr[q]; break; } }
|
|
8054
|
+
if (!prev || !next) return false;
|
|
8055
|
+
if (prev.id.indexOf("sep") === 0) return false;
|
|
8056
|
+
return true;
|
|
8057
|
+
});
|
|
8058
|
+
// now that the list exists: keep the menu inside the viewport
|
|
8059
|
+
var menuH = 12 + menuItems.length * 30;
|
|
8060
|
+
menuLeft = Math.max(8, Math.min(rowMenu.x, window.innerWidth - menuW - 8));
|
|
8061
|
+
menuTop = Math.max(8, Math.min(rowMenu.y, window.innerHeight - menuH - 8));
|
|
7230
8062
|
rowMenuEl = react_jsx_runtime.jsx("div", {
|
|
7231
8063
|
"data-dqt-menu": "on",
|
|
7232
8064
|
className: "dqt-rowmenu" + (menuClosing ? " dqt-rowmenu-closing" : ""),
|
|
@@ -7247,6 +8079,12 @@ window.__ModuleLoader__.load({
|
|
|
7247
8079
|
pointerEvents: menuClosing ? "none" : "auto"
|
|
7248
8080
|
},
|
|
7249
8081
|
children: menuItems.map(function (item) {
|
|
8082
|
+
if (item.id.indexOf("sep") === 0) {
|
|
8083
|
+
return react_jsx_runtime.jsx("div", {
|
|
8084
|
+
"data-dqt-menu-sep": "1",
|
|
8085
|
+
style: { height: 1, background: C.panelBorder, margin: "4px 6px", opacity: 0.7 }
|
|
8086
|
+
}, "menu-" + item.id);
|
|
8087
|
+
}
|
|
7250
8088
|
return react_jsx_runtime.jsx("button", {
|
|
7251
8089
|
role: "menuitem",
|
|
7252
8090
|
"data-dqt-menu-item": item.id,
|
|
@@ -7283,6 +8121,30 @@ window.__ModuleLoader__.load({
|
|
|
7283
8121
|
// bottom shadow lives (the clip would otherwise cut it off).
|
|
7284
8122
|
var SHEET_SHADOW_ROOM = 24;
|
|
7285
8123
|
var sheetUp = sheetOpen && chatViewActive;
|
|
8124
|
+
// The curtain's resize edge: a 10px strip along its bottom, with the pill
|
|
8125
|
+
// that says "this can be pulled". Only rendered while the curtain is up.
|
|
8126
|
+
var sheetGripEl = react_jsx_runtime.jsx("div", {
|
|
8127
|
+
"data-dqt-sheet-grip": "1",
|
|
8128
|
+
onPointerDown: sheetResizeStart,
|
|
8129
|
+
onPointerMove: sheetResizeMove,
|
|
8130
|
+
onPointerUp: sheetResizeEnd,
|
|
8131
|
+
onPointerCancel: sheetResizeEnd,
|
|
8132
|
+
title: T("sheet.resize"),
|
|
8133
|
+
style: {
|
|
8134
|
+
flex: "none", height: 10, width: "100%", cursor: "row-resize",
|
|
8135
|
+
touchAction: "none", display: "flex", alignItems: "center",
|
|
8136
|
+
justifyContent: "center", background: "transparent"
|
|
8137
|
+
},
|
|
8138
|
+
children: react_jsx_runtime.jsx("span", {
|
|
8139
|
+
style: {
|
|
8140
|
+
display: "block", width: 42, height: 4, borderRadius: 999,
|
|
8141
|
+
cornerShape: "round",
|
|
8142
|
+
// the one decorative spot that follows the theme colour: the grip is
|
|
8143
|
+
// the curtain's own control, and a neutral grey pill read as disabled
|
|
8144
|
+
background: C.accent, opacity: 0.35
|
|
8145
|
+
}
|
|
8146
|
+
})
|
|
8147
|
+
});
|
|
7286
8148
|
var sheetShellEl = react_jsx_runtime.jsx("div", {
|
|
7287
8149
|
key: "sheet-shell",
|
|
7288
8150
|
className: "dqt-sheet-clip",
|
|
@@ -7333,21 +8195,37 @@ window.__ModuleLoader__.load({
|
|
|
7333
8195
|
height: 0,
|
|
7334
8196
|
overflow: "visible"
|
|
7335
8197
|
},
|
|
7336
|
-
|
|
8198
|
+
// the panel's blur twin rides here, as a sibling of the curtain: the only
|
|
8199
|
+
// depth in this subtree where a backdrop-filter has been seen to work
|
|
8200
|
+
children: [panelGlassTwinEl, react_jsx_runtime.jsx("div", {
|
|
7337
8201
|
ref: sheetRef,
|
|
7338
8202
|
className: sheetUp ? "dqt-sheet dqt-sheet-open" : "dqt-sheet-idle",
|
|
7339
8203
|
"data-sheet": sheetUp ? "on" : undefined,
|
|
7340
8204
|
style: {
|
|
7341
8205
|
width: sheetUp ? "100%" : 0,
|
|
7342
|
-
// the bottom leaves the shadow room the clip box grew by
|
|
7343
|
-
|
|
8206
|
+
// the bottom leaves the shadow room the clip box grew by; the fraction
|
|
8207
|
+
// is the reader's own height (dragged from the bottom edge, remembered)
|
|
8208
|
+
height: sheetUp
|
|
8209
|
+
? "calc((100% - " + SHEET_SHADOW_ROOM + "px) * " + (sheetDragFrac === null ? prefs.sheetHeight : sheetDragFrac) + ")"
|
|
8210
|
+
: 0,
|
|
7344
8211
|
// border-box so the 1px edges stay INSIDE the measured height: with the
|
|
7345
8212
|
// old content-box the bottom pair sat below it and the clip cut them
|
|
7346
8213
|
// away, which is why the bottom edge had no visible line
|
|
7347
8214
|
boxSizing: "border-box",
|
|
7348
8215
|
display: "flex",
|
|
7349
8216
|
flexDirection: "column",
|
|
7350
|
-
|
|
8217
|
+
// The curtain is the transient surface (Windows' "Acrylic" case): the
|
|
8218
|
+
// live blur belongs here, where it is dismissed rather than lived in.
|
|
8219
|
+
// GATED ON sheetUp — this element is ALSO the docked panel's parent
|
|
8220
|
+
// while the curtain is away (height 0, yet still an ancestor), and a
|
|
8221
|
+
// backdrop-filter here promoted the whole subtree and dropped the
|
|
8222
|
+
// panel beneath the conversation. That was the real "panel disappears"
|
|
8223
|
+
// bug: the panel itself had no filter at all in that build.
|
|
8224
|
+
background: sheetUp ? C.sheetBg : "transparent",
|
|
8225
|
+
// L: a light/dark switch eases the material instead of snapping it
|
|
8226
|
+
transition: "background-color 0.25s ease",
|
|
8227
|
+
backdropFilter: sheetUp && C.glassOn ? C.sheetBlur : undefined,
|
|
8228
|
+
WebkitBackdropFilter: sheetUp && C.glassOn ? C.sheetBlur : undefined,
|
|
7351
8229
|
color: C.text,
|
|
7352
8230
|
border: "none",
|
|
7353
8231
|
borderTop: sheetUp ? "1px solid " + C.panelBorder : "none",
|
|
@@ -7358,17 +8236,25 @@ window.__ModuleLoader__.load({
|
|
|
7358
8236
|
// no dimming layer — the conversation below stays legible and usable.
|
|
7359
8237
|
// The outer shadow is the bottom edge's own: it follows the rounded
|
|
7360
8238
|
// bottom corners and stops before the vertical seams.
|
|
7361
|
-
borderRadius: "0 0
|
|
7362
|
-
|
|
8239
|
+
borderRadius: "0 0 " + RADIUS_SHEET + "px " + RADIUS_SHEET + "px",
|
|
8240
|
+
// the drop shadow lifts the sheet off the conversation; the inset pair
|
|
8241
|
+
// is the rim that says "this is a surface, not a haze"
|
|
8242
|
+
boxShadow: sheetUp ? (rimInset() + ", " + C.sheetShadow) : "none",
|
|
7363
8243
|
overflow: "hidden"
|
|
7364
8244
|
},
|
|
7365
|
-
|
|
7366
|
-
|
|
8245
|
+
// inside the curtain the SHEET is the glass (it has no opacity of its own,
|
|
8246
|
+
// so its backdrop-filter samples the page): no second twin here.
|
|
8247
|
+
// The grip is the curtain's own bottom edge: drag it to set the height.
|
|
8248
|
+
children: sheetOpen ? [panelEl, sheetGripEl] : [panelShadowEl, panelEl]
|
|
8249
|
+
})]
|
|
7367
8250
|
});
|
|
7368
8251
|
|
|
7369
8252
|
return react_dom.createPortal(
|
|
7370
8253
|
react_jsx_runtime.jsx(ErrorBoundary, {
|
|
7371
|
-
|
|
8254
|
+
// panelGlassTwinEl sits OUTSIDE the clipping wrapper on purpose: it is
|
|
8255
|
+
// the only place a backdrop-filter can sample the page without dragging
|
|
8256
|
+
// the docked panel under the conversation
|
|
8257
|
+
children: [panelGlassTwinEl, sheetShellEl, edgeEl, topHandleEl, pinnedEl, hoverEl, rowMenuEl]
|
|
7372
8258
|
}),
|
|
7373
8259
|
document.body
|
|
7374
8260
|
);
|
|
@@ -7463,7 +8349,16 @@ window.__ModuleLoader__.load({
|
|
|
7463
8349
|
ghost: false,
|
|
7464
8350
|
// what pinning and the row menu need to act on this very row
|
|
7465
8351
|
key: n.key,
|
|
7466
|
-
idx: n.idx
|
|
8352
|
+
idx: n.idx,
|
|
8353
|
+
level: n.level || 1,
|
|
8354
|
+
kind: "row",
|
|
8355
|
+
// "copy this section" is only offered when there IS a section below it:
|
|
8356
|
+
// for a heading with no sub-headings the entry would copy nothing but the
|
|
8357
|
+
// heading itself, which is what "copy heading" already does.
|
|
8358
|
+
hasKids: !!(n.children && n.children.length),
|
|
8359
|
+
// Built only if the reader actually picks "copy this section": walking a
|
|
8360
|
+
// subtree on every row of every render would be wasted work on a long log.
|
|
8361
|
+
mdOf: function () { return markdownOfNode(n).join("\n"); }
|
|
7467
8362
|
};
|
|
7468
8363
|
var rows;
|
|
7469
8364
|
if (wide) {
|
|
@@ -7532,6 +8427,69 @@ window.__ModuleLoader__.load({
|
|
|
7532
8427
|
// round backdrop appears, so the two boxes used to sit flush against each other
|
|
7533
8428
|
// (and on a long row the highlight ran underneath the button). This layer stops
|
|
7534
8429
|
// END_ZONE short of that edge, leaving a clear gap.
|
|
8430
|
+
// G: a hairline guide at this row's own indent, so H2/H3 read as children
|
|
8431
|
+
// without moving the text. Only rows below H1 carry one; the guide sits just
|
|
8432
|
+
// left of where the row's text starts.
|
|
8433
|
+
// The guide column depends on the LEVEL only. It must not share the text's
|
|
8434
|
+
// base inset: that inset differs for rows with children (2/4px) and without
|
|
8435
|
+
// (6/8px), which put rows of the same level 4px apart and made the lines
|
|
8436
|
+
// visibly ragged.
|
|
8437
|
+
var guideStep = wide ? 16 : 12;
|
|
8438
|
+
var guideOrigin = (wide ? 8 : 6) + MARK_INSET;
|
|
8439
|
+
if (n.level > 1) {
|
|
8440
|
+
rows.unshift(react_jsx_runtime.jsx("div", {
|
|
8441
|
+
"data-dqt-guide": "1",
|
|
8442
|
+
style: {
|
|
8443
|
+
position: "absolute",
|
|
8444
|
+
left: guideOrigin + (n.level - 2) * guideStep,
|
|
8445
|
+
top: 0,
|
|
8446
|
+
bottom: 0,
|
|
8447
|
+
width: 1,
|
|
8448
|
+
background: C.panelBorder,
|
|
8449
|
+
pointerEvents: "none",
|
|
8450
|
+
// the row leans in on hover; the guide column must not, or the line
|
|
8451
|
+
// visibly slides sideways every time the pointer crosses a row
|
|
8452
|
+
transition: "transform 0.15s ease"
|
|
8453
|
+
}
|
|
8454
|
+
}, uid + "-gd"));
|
|
8455
|
+
}
|
|
8456
|
+
// B: the bookmark. A ribbon, not a dot: a tab whose left edge is flush with
|
|
8457
|
+
// the row's left margin and whose right edge is cut inwards to a point — the
|
|
8458
|
+
// shape of a marker sticking out of the page, so it reads as "saved" rather
|
|
8459
|
+
// than as a bullet.
|
|
8460
|
+
//
|
|
8461
|
+
// ALWAYS RENDERED, faded by opacity: a layer that is created and destroyed pops
|
|
8462
|
+
// in and out, and saving a place is exactly the kind of small action that should
|
|
8463
|
+
// breathe (everything else in this panel fades).
|
|
8464
|
+
//
|
|
8465
|
+
// The colour is a concrete hex taken from the theme table, not a `var()`:
|
|
8466
|
+
// an SVG fill attribute silently rejects a var and paints black, and a colour
|
|
8467
|
+
// that has to be resolved by the compositor inside a clipped layer is one more
|
|
8468
|
+
// chance for that to happen again.
|
|
8469
|
+
var marked = !!(extra && extra.isMarked && extra.isMarked(n.key, n.idx));
|
|
8470
|
+
rows.unshift(react_jsx_runtime.jsx("div", {
|
|
8471
|
+
"data-dqt-mark": marked ? "1" : undefined,
|
|
8472
|
+
style: {
|
|
8473
|
+
position: "absolute",
|
|
8474
|
+
// Inside the row's own box on purpose: the row clips its content (it
|
|
8475
|
+
// needs `overflow: hidden` for the title's ellipsis), so a ribbon hung
|
|
8476
|
+
// outside it was cut down to a sliver. The row itself is pulled left
|
|
8477
|
+
// instead (see marginLeft below), which is what puts this at the panel
|
|
8478
|
+
// edge while still being painted by the row.
|
|
8479
|
+
left: 0,
|
|
8480
|
+
top: "50%",
|
|
8481
|
+
transform: "translateY(-50%)",
|
|
8482
|
+
width: MARK_W,
|
|
8483
|
+
height: 12,
|
|
8484
|
+
background: (extra && extra.markColor) || C.accent,
|
|
8485
|
+
// flat on the panel side, a concave point cut into the other side
|
|
8486
|
+
clipPath: "polygon(0 0, 100% 0, 70% 50%, 100% 100%, 0 100%)",
|
|
8487
|
+
WebkitClipPath: "polygon(0 0, 100% 0, 70% 50%, 100% 100%, 0 100%)",
|
|
8488
|
+
opacity: marked ? 1 : 0,
|
|
8489
|
+
transition: "opacity 0.24s ease",
|
|
8490
|
+
pointerEvents: "none"
|
|
8491
|
+
}
|
|
8492
|
+
}, uid + "-mk"));
|
|
7535
8493
|
rows.unshift(react_jsx_runtime.jsx("div", {
|
|
7536
8494
|
"data-dqt-hl": "1",
|
|
7537
8495
|
style: {
|
|
@@ -7555,6 +8513,11 @@ window.__ModuleLoader__.load({
|
|
|
7555
8513
|
if (hl) hl.style.background = C.hover;
|
|
7556
8514
|
var b = e.currentTarget.querySelector("[data-dqt-end]");
|
|
7557
8515
|
if (b) b.style.opacity = "1";
|
|
8516
|
+
// I: the row leans in by one pixel — enough to feel alive, small enough
|
|
8517
|
+
// that the text never looks like it jumped
|
|
8518
|
+
e.currentTarget.style.transform = "translateX(1px)";
|
|
8519
|
+
var gd = e.currentTarget.querySelector("[data-dqt-guide]");
|
|
8520
|
+
if (gd) gd.style.transform = "translateX(-1px)";
|
|
7558
8521
|
if (extra && extra.hoverStart) extra.hoverStart(hoverInfo, e.currentTarget);
|
|
7559
8522
|
},
|
|
7560
8523
|
onMouseLeave: function (e) {
|
|
@@ -7562,6 +8525,9 @@ window.__ModuleLoader__.load({
|
|
|
7562
8525
|
if (hl) hl.style.background = "transparent";
|
|
7563
8526
|
var b = e.currentTarget.querySelector("[data-dqt-end]");
|
|
7564
8527
|
if (b) b.style.opacity = "0";
|
|
8528
|
+
e.currentTarget.style.transform = "translateX(0)";
|
|
8529
|
+
var gd2 = e.currentTarget.querySelector("[data-dqt-guide]");
|
|
8530
|
+
if (gd2) gd2.style.transform = "translateX(0)";
|
|
7565
8531
|
if (extra && extra.hoverEnd) extra.hoverEnd();
|
|
7566
8532
|
},
|
|
7567
8533
|
onContextMenu: function (e) {
|
|
@@ -7578,10 +8544,20 @@ window.__ModuleLoader__.load({
|
|
|
7578
8544
|
position: "relative",
|
|
7579
8545
|
padding: dense ? (wide ? "1px 8px" : "0px 6px") : wide ? "4px 8px" : "2px 6px",
|
|
7580
8546
|
paddingRight: END_ZONE,
|
|
7581
|
-
|
|
8547
|
+
// the ribbon gutter is always reserved (see MARK_INSET): saving a row must
|
|
8548
|
+
// never move the text of that row, or of every other row after it
|
|
8549
|
+
paddingLeft: MARK_INSET + (hasChildren ? (wide ? 4 : 2) : (wide ? 8 : 6)) + (n.level - 1) * (wide ? 16 : 12),
|
|
7582
8550
|
margin: dense ? "0 0" : "1px 0",
|
|
8551
|
+
// Reach back over the group box's left padding so the ribbon gutter sits
|
|
8552
|
+
// at the surface's edge. The group clips its contents, so this is as far
|
|
8553
|
+
// left as anything on a row can go — and the row is still inside the
|
|
8554
|
+
// group's box, so nothing is cut.
|
|
8555
|
+
marginLeft: -(wide ? 8 : 7),
|
|
7583
8556
|
borderRadius: 6,
|
|
7584
8557
|
cursor: "pointer",
|
|
8558
|
+
// I: a 1px nudge on hover (no reflow — transform only), plus the panel's
|
|
8559
|
+
// own background transition so a light/dark switch eases instead of snapping
|
|
8560
|
+
transition: "transform 0.15s ease, background-color 0.25s ease",
|
|
7585
8561
|
fontSize: n.level <= 2 ? (wide ? 16.5 : 13) : (wide ? 15.5 : 12),
|
|
7586
8562
|
color: n.level <= 2 ? C.text : C.muted,
|
|
7587
8563
|
fontWeight: n.level <= 2 ? 600 : 400,
|
|
@@ -7858,6 +8834,10 @@ window.__ModuleLoader__.load({
|
|
|
7858
8834
|
var lastKey = (g.msgs && g.msgs.length > 0) ? g.msgs[g.msgs.length - 1].key
|
|
7859
8835
|
: (g.failure ? g.failure.key : "");
|
|
7860
8836
|
return react_jsx_runtime.jsx("div", {
|
|
8837
|
+
// the day strip above the list reads these two attributes on scroll: the
|
|
8838
|
+
// label that has scrolled past the top edge is the one it shows
|
|
8839
|
+
"data-dqt-grouplabel": "1",
|
|
8840
|
+
"data-dqt-day": g.time !== undefined ? String(fmtStampOf(g.time)) : "",
|
|
7861
8841
|
onMouseEnter: function (e) {
|
|
7862
8842
|
var b = e.currentTarget.querySelector("[data-dqt-end]");
|
|
7863
8843
|
if (b) b.style.opacity = "1";
|
|
@@ -7866,6 +8846,26 @@ window.__ModuleLoader__.load({
|
|
|
7866
8846
|
var b = e.currentTarget.querySelector("[data-dqt-end]");
|
|
7867
8847
|
if (b) b.style.opacity = "0";
|
|
7868
8848
|
},
|
|
8849
|
+
// The time row takes the same menu as an outline row, minus the two entries
|
|
8850
|
+
// that need a single preview card / single heading (pin, copy heading).
|
|
8851
|
+
// "Copy this section" here means every heading under this turn.
|
|
8852
|
+
onContextMenu: function (e) {
|
|
8853
|
+
if (!extra || !extra.rowMenu) return;
|
|
8854
|
+
var anchorKey = replyKey || g.userKey || "";
|
|
8855
|
+
extra.rowMenu(e, {
|
|
8856
|
+
title: (g.time ? String(g.time) : "") + (g.userText ? " · " + g.userText : ""),
|
|
8857
|
+
sub: "",
|
|
8858
|
+
preview: "",
|
|
8859
|
+
meta: "",
|
|
8860
|
+
ghost: false,
|
|
8861
|
+
key: anchorKey,
|
|
8862
|
+
idx: 0,
|
|
8863
|
+
level: 0,
|
|
8864
|
+
kind: "group",
|
|
8865
|
+
canCopy: !!(g.headings && g.headings.length),
|
|
8866
|
+
mdOf: function () { return markdownOfGroup(g); }
|
|
8867
|
+
}, anchorKey, 0);
|
|
8868
|
+
},
|
|
7869
8869
|
style: {
|
|
7870
8870
|
// ONE row geometry for every group header, whether or not the turn has
|
|
7871
8871
|
// headings: same box, same 18px rhythm, same left inset, so the time
|
|
@@ -7883,15 +8883,20 @@ window.__ModuleLoader__.load({
|
|
|
7883
8883
|
display: "flex",
|
|
7884
8884
|
alignItems: "center",
|
|
7885
8885
|
minWidth: 0,
|
|
7886
|
-
// sticky
|
|
7887
|
-
//
|
|
7888
|
-
//
|
|
7889
|
-
//
|
|
7890
|
-
//
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
8886
|
+
// NOT sticky since the surface became translucent (0.10.0): a label that
|
|
8887
|
+
// has to occlude the rows scrolling under it needs an opaque fill, and an
|
|
8888
|
+
// opaque fill on a see-through panel is exactly the horizontal band that
|
|
8889
|
+
// reads as a cut. Letting the label scroll away keeps ONE transparency
|
|
8890
|
+
// across the whole list; the current date moved into the title bar (which
|
|
8891
|
+
// is chrome, and therefore legitimately solid).
|
|
8892
|
+
// The "current group" tint still rides on its own layer inside the row,
|
|
8893
|
+
// which can FADE (a background-image gradient cannot be transitioned).
|
|
8894
|
+
// RELATIVE, not static: that tint layer is `position:absolute; inset:0`,
|
|
8895
|
+
// so it needs this row as its containing block. As a static row the tint
|
|
8896
|
+
// resolved against the panel itself and washed the whole list in the
|
|
8897
|
+
// accent colour for a frame (then the active group settled and it went).
|
|
8898
|
+
position: "relative",
|
|
8899
|
+
background: "transparent"
|
|
7895
8900
|
},
|
|
7896
8901
|
children: [
|
|
7897
8902
|
react_jsx_runtime.jsx("div", {
|
|
@@ -8133,10 +9138,16 @@ window.__ModuleLoader__.load({
|
|
|
8133
9138
|
borderTop: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
8134
9139
|
borderRight: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
8135
9140
|
borderBottom: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
8136
|
-
|
|
9141
|
+
// The accent bar is a LAYER, not a border: it needs to be a gradient and
|
|
9142
|
+
// to fade with the rest of the box. It cannot sit in a transparent left
|
|
9143
|
+
// border, though — this box carries `content-visibility: auto`, which
|
|
9144
|
+
// implies `contain: paint`, and paint containment clips descendants to
|
|
9145
|
+
// the PADDING box (the bar was clipped away entirely). So the 3px the
|
|
9146
|
+
// border used to take is padding instead, and the bar lives inside it.
|
|
9147
|
+
// Same content position: 5px bar + 2px padding == 7px padding.
|
|
9148
|
+
paddingLeft: 7,
|
|
8137
9149
|
borderRadius: 6,
|
|
8138
9150
|
margin: dense ? "0 -5px" : "2px -5px",
|
|
8139
|
-
paddingLeft: 2,
|
|
8140
9151
|
// Long-session virtualization: every offscreen group is skipped by the
|
|
8141
9152
|
// renderer (no layout, no paint) until it nears the viewport, while its
|
|
8142
9153
|
// DOM stays in place — so the row queries the follow, the keyboard
|
|
@@ -8146,15 +9157,88 @@ window.__ModuleLoader__.load({
|
|
|
8146
9157
|
// While a surface is ANIMATING the hold lifts the skip entirely:
|
|
8147
9158
|
// a compositor-driven slide does not re-evaluate the skip state, so
|
|
8148
9159
|
// an opening surface would otherwise stay blank until it lands.
|
|
9160
|
+
position: "relative",
|
|
8149
9161
|
contentVisibility: extra && extra.virtHold ? "visible" : "auto",
|
|
8150
9162
|
containIntrinsicSize: "auto " + (dense ? 80 : 96) + "px"
|
|
8151
9163
|
},
|
|
8152
|
-
children:
|
|
9164
|
+
children: [
|
|
9165
|
+
// H: the accent bar, as a layer so it can be a gradient AND fade with the
|
|
9166
|
+
// rest of the box. It sits inside the 3px transparent left border, so the
|
|
9167
|
+
// row geometry is identical in both states.
|
|
9168
|
+
react_jsx_runtime.jsx("div", {
|
|
9169
|
+
key: "g-bar",
|
|
9170
|
+
style: {
|
|
9171
|
+
position: "absolute",
|
|
9172
|
+
left: 0,
|
|
9173
|
+
top: 0,
|
|
9174
|
+
bottom: 0,
|
|
9175
|
+
width: 5,
|
|
9176
|
+
background: "linear-gradient(to bottom, " + C.groupEdge + ", " + C.groupEdgeSoft + ")",
|
|
9177
|
+
opacity: isActive ? 1 : 0,
|
|
9178
|
+
transition: "opacity 0.4s ease",
|
|
9179
|
+
pointerEvents: "none"
|
|
9180
|
+
}
|
|
9181
|
+
})
|
|
9182
|
+
].concat(items)
|
|
8153
9183
|
}, "g-" + gi));
|
|
8154
9184
|
}
|
|
8155
9185
|
return out;
|
|
8156
9186
|
}
|
|
8157
9187
|
|
|
9188
|
+
// A heading and everything under it, as Markdown. Only headings: this is the
|
|
9189
|
+
// outline below this entry, not a transcript of the section's prose. The entry
|
|
9190
|
+
// is offered only when there IS something below (see the row menu), so an empty
|
|
9191
|
+
// result can never be pasted by accident.
|
|
9192
|
+
function markdownOfNode(n) {
|
|
9193
|
+
var out = [];
|
|
9194
|
+
var lvl = Math.min(6, Math.max(1, n.level || 1));
|
|
9195
|
+
var hashes = "";
|
|
9196
|
+
for (var h = 0; h < lvl; h++) hashes += "#";
|
|
9197
|
+
out.push(hashes + " " + (n.title || ""));
|
|
9198
|
+
var kids = n.children || [];
|
|
9199
|
+
for (var i = 0; i < kids.length; i++) {
|
|
9200
|
+
out = out.concat(markdownOfNode(kids[i]));
|
|
9201
|
+
}
|
|
9202
|
+
return out;
|
|
9203
|
+
}
|
|
9204
|
+
// The same thing for a whole turn: every heading under one time row, in order.
|
|
9205
|
+
function markdownOfGroup(g) {
|
|
9206
|
+
var hs = (g && g.headings) || [];
|
|
9207
|
+
var out = [];
|
|
9208
|
+
for (var i = 0; i < hs.length; i++) {
|
|
9209
|
+
var lvl = Math.min(6, Math.max(1, hs[i].level || 1));
|
|
9210
|
+
var hashes = "";
|
|
9211
|
+
for (var h = 0; h < lvl; h++) hashes += "#";
|
|
9212
|
+
out.push(hashes + " " + (hs[i].title || ""));
|
|
9213
|
+
}
|
|
9214
|
+
return out.join("\n");
|
|
9215
|
+
}
|
|
9216
|
+
// Put text on the clipboard. The async API needs a secure context (the panel
|
|
9217
|
+
// runs on the host page, which is loopback or https) and is unavailable on a
|
|
9218
|
+
// plain-http remote visit, so the execCommand path is the fallback rather than
|
|
9219
|
+
// an afterthought.
|
|
9220
|
+
var copyText = function (text) {
|
|
9221
|
+
try {
|
|
9222
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
|
|
9223
|
+
navigator.clipboard.writeText(String(text));
|
|
9224
|
+
return true;
|
|
9225
|
+
}
|
|
9226
|
+
} catch (e) { /* fall through to the legacy path */ }
|
|
9227
|
+
try {
|
|
9228
|
+
var ta = document.createElement("textarea");
|
|
9229
|
+
ta.value = String(text);
|
|
9230
|
+
ta.setAttribute("readonly", "");
|
|
9231
|
+
ta.style.position = "fixed";
|
|
9232
|
+
ta.style.top = "-1000px";
|
|
9233
|
+
ta.style.opacity = "0";
|
|
9234
|
+
document.body.appendChild(ta);
|
|
9235
|
+
if (ta.select) ta.select();
|
|
9236
|
+
var ok = typeof document.execCommand === "function" ? document.execCommand("copy") : false;
|
|
9237
|
+
document.body.removeChild(ta);
|
|
9238
|
+
return !!ok;
|
|
9239
|
+
} catch (e) { return false; }
|
|
9240
|
+
};
|
|
9241
|
+
|
|
8158
9242
|
// Host-facing locale namespace: the slot declares `locale: "dsh-quick-toc"`, and
|
|
8159
9243
|
// registering BOTH tables under the same keys is what lets the panel follow the
|
|
8160
9244
|
// host's language: `T` asks the host translation first and only falls back to the
|
|
@@ -8216,6 +9300,43 @@ window.__ModuleLoader__.load({
|
|
|
8216
9300
|
return seg([["on", T("settings.on")], ["off", T("settings.off")]], on ? "on" : "off",
|
|
8217
9301
|
function (v) { onToggle(v === "on"); });
|
|
8218
9302
|
};
|
|
9303
|
+
// Theme colour dots: the choice IS a colour, so it looks like one. Inline
|
|
9304
|
+
// styles (no stylesheet dependency): a 22px dot per option, the selected
|
|
9305
|
+
// one ringed. The dot shows the vivid hue; text roles use the readable
|
|
9306
|
+
// solid from the same option (see ACCENTS). Labels are listed statically
|
|
9307
|
+
// (like every seg row) so the dictionary check sees each key.
|
|
9308
|
+
var accentDots = function (current, onPick) {
|
|
9309
|
+
var pairs = [["blue", T("settings.accent.blue")], ["red", T("settings.accent.red")],
|
|
9310
|
+
["yellow", T("settings.accent.yellow")], ["green", T("settings.accent.green")],
|
|
9311
|
+
["purple", T("settings.accent.purple")], ["pink", T("settings.accent.pink")]];
|
|
9312
|
+
return react_jsx_runtime.jsx("div", {
|
|
9313
|
+
className: "dqt-dots",
|
|
9314
|
+
role: "radiogroup",
|
|
9315
|
+
"aria-label": T("settings.accent"),
|
|
9316
|
+
style: { display: "flex", gap: "10px", alignItems: "center", padding: "2px" },
|
|
9317
|
+
children: pairs.map(function (pair) {
|
|
9318
|
+
var id = pair[0];
|
|
9319
|
+
var on = current === id;
|
|
9320
|
+
var dot = (ACCENTS[id] || ACCENTS.blue).dot;
|
|
9321
|
+
return react_jsx_runtime.jsx("button", {
|
|
9322
|
+
type: "button",
|
|
9323
|
+
role: "radio",
|
|
9324
|
+
"aria-checked": on,
|
|
9325
|
+
title: pair[1],
|
|
9326
|
+
"aria-label": pair[1],
|
|
9327
|
+
disabled: !writable,
|
|
9328
|
+
onClick: function () { onPick(id); },
|
|
9329
|
+
className: "dqt-dot" + (on ? " dqt-dotOn" : ""),
|
|
9330
|
+
style: {
|
|
9331
|
+
width: "22px", height: "22px", borderRadius: "50%", padding: 0,
|
|
9332
|
+
border: "none", cursor: writable ? "pointer" : "default",
|
|
9333
|
+
background: dot, opacity: writable ? 1 : 0.4,
|
|
9334
|
+
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)"
|
|
9335
|
+
}
|
|
9336
|
+
}, id);
|
|
9337
|
+
})
|
|
9338
|
+
});
|
|
9339
|
+
};
|
|
8219
9340
|
// Percentage sliders (the panel scale and the handle position) — the same
|
|
8220
9341
|
// control shape as the fonttune card's slider. Dragging only moves a LOCAL
|
|
8221
9342
|
// value: the settings document is written once the pointer is released (a
|
|
@@ -8230,7 +9351,9 @@ window.__ModuleLoader__.load({
|
|
|
8230
9351
|
sliderAwaiting.current[field] = pct;
|
|
8231
9352
|
commit(field === "zoom" ? { zoom: pct / 100 }
|
|
8232
9353
|
: field === "sheetZoom" ? { sheetZoom: pct / 100 }
|
|
8233
|
-
|
|
9354
|
+
// this one is stored as the 0..100 it reads as, not as a ratio
|
|
9355
|
+
: field === "glassStrength" ? { glassStrength: pct }
|
|
9356
|
+
: { handle: pct / 100 });
|
|
8234
9357
|
};
|
|
8235
9358
|
react.useEffect(function () {
|
|
8236
9359
|
var a = sliderAwaiting.current;
|
|
@@ -8238,8 +9361,9 @@ window.__ModuleLoader__.load({
|
|
|
8238
9361
|
if (a.zoom !== undefined && Math.round(prefs.zoom * 100) === a.zoom) { delete a.zoom; settled = true; }
|
|
8239
9362
|
if (a.sheetZoom !== undefined && Math.round(prefs.sheetZoom * 100) === a.sheetZoom) { delete a.sheetZoom; settled = true; }
|
|
8240
9363
|
if (a.handle !== undefined && Math.round(prefs.handle * 100) === a.handle) { delete a.handle; settled = true; }
|
|
9364
|
+
if (a.glassStrength !== undefined && Math.round(prefs.glassStrength) === a.glassStrength) { delete a.glassStrength; settled = true; }
|
|
8241
9365
|
if (settled) setSliderDrag(null);
|
|
8242
|
-
}, [prefs.zoom, prefs.sheetZoom, prefs.handle]);
|
|
9366
|
+
}, [prefs.zoom, prefs.sheetZoom, prefs.handle, prefs.glassStrength]);
|
|
8243
9367
|
react.useEffect(function () {
|
|
8244
9368
|
if (sliderDrag === null) return undefined;
|
|
8245
9369
|
var release = function () {
|
|
@@ -8291,6 +9415,7 @@ window.__ModuleLoader__.load({
|
|
|
8291
9415
|
var zoomControl = sliderCtl("zoom", "settings.zoom", 50, 200, 5, Math.round(prefs.zoom * 100));
|
|
8292
9416
|
var sheetZoomControl = sliderCtl("sheetZoom", "settings.sheetZoom", 50, 200, 5, Math.round(prefs.sheetZoom * 100));
|
|
8293
9417
|
var handleControl = sliderCtl("handle", "settings.handle", 0, 100, 1, Math.round(prefs.handle * 100));
|
|
9418
|
+
var glassStrengthControl = sliderCtl("glassStrength", "settings.glassStrength", 0, 100, 5, clampPct(prefs.glassStrength, 60));
|
|
8294
9419
|
var field = function (key, label, hint, control, inline, last) {
|
|
8295
9420
|
var overridden = Object.prototype.hasOwnProperty.call(user, key);
|
|
8296
9421
|
var reset = overridden ? react_jsx_runtime.jsx("button", {
|
|
@@ -8353,6 +9478,12 @@ window.__ModuleLoader__.load({
|
|
|
8353
9478
|
field("density", T("settings.density"), "",
|
|
8354
9479
|
seg([["comfy", T("settings.density.comfy")], ["compact", T("settings.density.compact")]], prefs.density,
|
|
8355
9480
|
function (v) { commit({ density: v }); }), true),
|
|
9481
|
+
field("accent", T("settings.accent"), "",
|
|
9482
|
+
accentDots(prefs.accent, function (v) { commit({ accent: v }); }), true),
|
|
9483
|
+
field("glass", T("settings.glass"),
|
|
9484
|
+
T("settings.glass.tip") + (autoGlassOff ? " " + T("settings.glass.autoOff") : ""),
|
|
9485
|
+
onoff(prefs.glass, function (v) { commit({ glass: v }); }), true),
|
|
9486
|
+
field("glassStrength", T("settings.glassStrength"), T("settings.glassStrength.tip"), glassStrengthControl, false),
|
|
8356
9487
|
field("zoom", T("settings.zoom"), T("settings.zoom.tip"), zoomControl, false),
|
|
8357
9488
|
field("sheetZoom", T("settings.sheetZoom"), T("settings.sheetZoom.tip"), sheetZoomControl, false),
|
|
8358
9489
|
field("handle", T("settings.handle"), T("settings.handle.tip"), handleControl, false),
|
|
@@ -8697,6 +9828,10 @@ window.__ModuleLoader__.load({
|
|
|
8697
9828
|
exports.name = "dsh-quick-toc"; // runner attributes slot entries to this registrant
|
|
8698
9829
|
exports.apply = apply;
|
|
8699
9830
|
exports.inject = inject;
|
|
9831
|
+
// Which build the page actually loaded. A stale client bundle looks exactly
|
|
9832
|
+
// like a broken feature, so this one string settles it in a single look:
|
|
9833
|
+
// type __dqtBuild in the console.
|
|
9834
|
+
try { if (typeof window !== "undefined") window.__dqtBuild = "0.10.0-glass-card"; } catch (e) {}
|
|
8700
9835
|
|
|
8701
9836
|
// inject the small keyframe + panel-scrollbar stylesheet once
|
|
8702
9837
|
(function () {
|
|
@@ -8716,7 +9851,9 @@ window.__ModuleLoader__.load({
|
|
|
8716
9851
|
// card that is ADOPTED from the hover card (it is already on screen, so it must
|
|
8717
9852
|
// not re-play the entrance and blink).
|
|
8718
9853
|
".dqt-hover{animation:dqt-fade-in 0.12s linear both}" +
|
|
8719
|
-
|
|
9854
|
+
// the bookmark view is a different list riding on top of the outline: it eases
|
|
9855
|
+
// in rather than appearing between two frames
|
|
9856
|
+
".dqt-marks-overlay{animation:dqt-fade-in 0.16s linear both}" + ".dqt-hover-static{animation:none}" +
|
|
8720
9857
|
".dqt-hover-closing{animation:dqt-fade-out 0.16s cubic-bezier(0.2, 0.9, 0.3, 1) both;pointer-events:none}" +
|
|
8721
9858
|
// the outline row menu: same vocabulary as the level picker (a small pop)
|
|
8722
9859
|
".dqt-rowmenu{animation:dqt-pop-in 0.14s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
|
|
@@ -8734,9 +9871,25 @@ window.__ModuleLoader__.load({
|
|
|
8734
9871
|
// on the way out — the same vocabulary as the docked panel sliding in from its
|
|
8735
9872
|
// edge. The clip box it lives in (see the portal) is what keeps it from being
|
|
8736
9873
|
// seen above that line, so it reads as emerging from under the view tabs.
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
9874
|
+
// K: the curtain drops a hair past its resting line and settles back, so it
|
|
9875
|
+
// reads as a sheet landing rather than a panel arriving at a coordinate.
|
|
9876
|
+
// The overshoot is small and late (88%) so the settle is a long, soft
|
|
9877
|
+
// exhale — at 70%/6px it read as a hard snap.
|
|
9878
|
+
// K: the curtain drops a hair past its resting line and settles back.
|
|
9879
|
+
// The settle gets its OWN timing function and a long segment (62% -> 100%):
|
|
9880
|
+
// with the overshoot at 88% the return lasted 55ms and, because the drop's
|
|
9881
|
+
// ease-out is fast at a segment's start, it read as a snap back into place.
|
|
9882
|
+
// At the turning point the velocity is zero, so that leg eases IN as well as
|
|
9883
|
+
// out — that is what makes it a settle instead of a bounce.
|
|
9884
|
+
"@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)}}" +
|
|
9885
|
+
// The retract has to clear the shadow too, not just the sheet: the sheet is
|
|
9886
|
+
// 24px shorter than its shell (that strip is where the drop shadow lands),
|
|
9887
|
+
// so `translateY(-100%)` parked its own shadow exactly at the shell's top
|
|
9888
|
+
// edge and the state flip 40ms later blinked that band away.
|
|
9889
|
+
"@keyframes dqt-sheet-out{from{transform:translateY(0)}to{transform:translateY(calc(-100% - 24px))}}" +
|
|
9890
|
+
// 0.5s: the drop is 62% of it and the settle the rest, so the settle is a
|
|
9891
|
+
// readable ~190ms rather than a two-frame correction
|
|
9892
|
+
".dqt-sheet-open{animation:dqt-sheet-in 0.5s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform}" +
|
|
8740
9893
|
".dqt-sheet-closing{animation:dqt-sheet-out 0.3s cubic-bezier(0.4, 0, 1, 1) both;pointer-events:none;will-change:transform}" +
|
|
8741
9894
|
// The curtain's own drop and retract are deliberately NOT gated on
|
|
8742
9895
|
// `prefers-reduced-motion`: with the host system's animations switched off (Windows
|