dsh-quick-toc 0.10.2 → 0.10.4
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 +22 -0
- package/CHANGELOG.md +22 -0
- package/README.en.md +13 -1
- package/README.md +13 -1
- package/icon.svg +4 -4
- package/lib/client.js +579 -69
- package/lib/index.js +11 -4
- package/package.json +9 -18
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-quick-toc** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
|
|
4
4
|
|
|
5
|
+
## [0.10.4] - 2026-11-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Choice of views**: a new "Views" setting switches between panel + curtain, panel only and curtain only; with "curtain only" the docked panel is no longer built.
|
|
9
|
+
- **Position indicator**: a column of ticks and a slider on the right of the outline shows how far into the conversation you are; the style is either a bar or a soft band, and clicking a tick jumps to that section.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **The declared host range now starts at 0.2.1-rc.2**: earlier prereleases are no longer listed as supported.
|
|
13
|
+
- **In the curtain the outline and the position indicator travel as one column**: the column is centred and moves with the search and bookmark sidebars, so the indicator stays beside the outline.
|
|
14
|
+
- **The "auto-load history" setting is gone**: history loads one page each time you reach the top, with no switch.
|
|
15
|
+
- **Reaching the top loads one page per arrival**: the wheel and the slider follow the same rule, matching the host's own "load earlier".
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Translucency stayed off for good on a slow device**: after it has been switched off automatically, turning the switch back on now takes effect.
|
|
19
|
+
- **Dragging the slider to the top left it stuck there**: it can be dragged back down again.
|
|
20
|
+
- **Unloaded rows appeared while scrolling up**: a page no longer leaves placeholder rows behind.
|
|
21
|
+
|
|
22
|
+
## [0.10.3] - 2026-10-28
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Supports DSH 0.2.1-rc.3, 0.2.1-rc.4 and 0.2.2-alpha.2**: no plugin code changed; the compatibility declaration registers the three prereleases, all verified item by item in an isolated environment.
|
|
26
|
+
|
|
5
27
|
## [0.10.2] - 2026-10-22
|
|
6
28
|
|
|
7
29
|
### Added
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-quick-toc** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.10.4] - 2026-11-04
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- **视图选择**:设置里新增「视图」,可选「面板 + 幕布」「仅面板」「仅幕布」;选「仅幕布」时不再构建停靠面板。
|
|
9
|
+
- **位置指示**:大纲右侧新增一列刻度与滑块,显示读到会话的哪个位置;样式可选「竖条」或「淡框」,点击刻度可跳到该节。
|
|
10
|
+
|
|
11
|
+
### 变更
|
|
12
|
+
- **兼容范围收窄到 0.2.1-rc.2 及以上**:更早的预发布版本不再声明为兼容。
|
|
13
|
+
- **幕布里的大纲与位置指示成为一列**:整列居中,打开搜索或书签侧栏时一起移动,位置指示始终贴着大纲。
|
|
14
|
+
- **「自动加载历史」设置已移除**:历史改为每次到达顶部时加载一页,不再有开关。
|
|
15
|
+
- **到达顶部时一次只加载一页**:滚轮和拖动滑块同样按页加载,与宿主的「加载更早」一致。
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
- **设备较慢时半透明被永久关闭**:自动关闭后,手动重新打开开关不再被此前记下的结果挡住。
|
|
19
|
+
- **把滑块拖到顶部后无法再向下拖**:现在可以正常拖回。
|
|
20
|
+
- **向上滚动时出现未加载的条目**:加载一页之后不再出现只占位的条目。
|
|
21
|
+
|
|
22
|
+
## [0.10.3] - 2026-10-28
|
|
23
|
+
|
|
24
|
+
### 变更
|
|
25
|
+
- **支持 DSH 0.2.1-rc.3、0.2.1-rc.4 与 0.2.2-alpha.2**:插件代码未改动,兼容性声明里登记了这三个预发布版本;隔离环境里逐项实测通过。
|
|
26
|
+
|
|
5
27
|
## [0.10.2] - 2026-10-22
|
|
6
28
|
|
|
7
29
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH): it turns the Markdown headings (H1–H6) of AI replies into a navigable outline — the same list as a side panel or as a full-width curtain — grouped by conversation turn and covering the whole session (including turns that are not loaded yet), with title/prompts/full-text/cross-session search, hover previews, a right-click menu for jumping and copying, bookmarks with a bookmarks-only view, six theme colours and a translucent material, and a reading position that both follows and is remembered.
|
|
6
6
|
|
|
7
|
+
## Screenshots
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
| The curtain (full-width view) | Its bookmarks drawer |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
|  |  |
|
|
14
|
+
|
|
15
|
+
> Captured from an isolated instance against a demo conversation.
|
|
16
|
+
|
|
7
17
|
## Features
|
|
8
18
|
|
|
9
19
|
- **Turn-grouped outline** — each user message plus its following AI replies form one group; the group header shows the turn's time and a first-line preview, and clicking it jumps to the start of that turn's model reply
|
|
@@ -44,7 +54,7 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
|
|
|
44
54
|
|
|
45
55
|
| Plugin | Supported DSH |
|
|
46
56
|
| --- | --- |
|
|
47
|
-
| **0.10.x** (latest, 0.10.
|
|
57
|
+
| **0.10.x** (latest, 0.10.4) | 0.2.1-rc.2, 0.2.1-rc.3, 0.2.1-rc.4, 0.2.1-rc.5, 0.2.2-alpha.2 |
|
|
48
58
|
| **0.9.x** (0.9.5) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5 |
|
|
49
59
|
| 0.8.x (0.8.2) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2 |
|
|
50
60
|
| 0.7.x (0.7.14) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2 |
|
|
@@ -56,6 +66,8 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
|
|
|
56
66
|
|
|
57
67
|
Each major line lists only its newest patch (the defects a new feature introduces are fixed in the patches that follow, so within one major line the newest patch is the one to use; older patches keep working — the plugin does not break existing interfaces).
|
|
58
68
|
|
|
69
|
+
From 0.10.4 the declared floor is 0.2.1-rc.2: earlier pre-releases are no longer supported — they can no longer open a session written by the current line, so the plugin's behaviour there cannot be verified.
|
|
70
|
+
|
|
59
71
|
## Install
|
|
60
72
|
|
|
61
73
|
With the DSH CLI:
|
package/README.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
**DeepSeek Harness(DSH)对话大纲插件**:把 AI 回复中的 Markdown 标题(H1–H6)提取成可导航的大纲——侧边面板与全屏幕布两种视图,按对话回合分组、覆盖整段会话(含尚未加载进窗口的回合)。支持标题/提问/全文/跨会话四档检索、悬停预览、右键跳转与复制、书签与「只看书签」视图,六种主题色与半透明材质,阅读位置自动跟随并可记住。
|
|
6
6
|
|
|
7
|
+
## 效果
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
| 幕布(全宽视图) | 幕布右侧的书签栏 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
|  |  |
|
|
14
|
+
|
|
15
|
+
> 截图取自隔离环境里的实例,会话内容是用作演示的示例文案。
|
|
16
|
+
|
|
7
17
|
## 功能
|
|
8
18
|
|
|
9
19
|
- **按回合分组** —— 每条用户消息 + 其后续 AI 回复为一组,组头显示回合时间与首行预览,点击跳到该回合模型回答的开头
|
|
@@ -44,7 +54,7 @@
|
|
|
44
54
|
|
|
45
55
|
| 插件版本 | 支持的 DSH 版本 |
|
|
46
56
|
| --- | --- |
|
|
47
|
-
| **0.10.x**(最新,0.10.
|
|
57
|
+
| **0.10.x**(最新,0.10.4) | 0.2.1-rc.2、0.2.1-rc.3、0.2.1-rc.4、0.2.1-rc.5、0.2.2-alpha.2 |
|
|
48
58
|
| **0.9.x**(0.9.5) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5 |
|
|
49
59
|
| 0.8.x(0.8.2) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2 |
|
|
50
60
|
| 0.7.x(0.7.14) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2 |
|
|
@@ -56,6 +66,8 @@
|
|
|
56
66
|
|
|
57
67
|
每个大版本只列该系列最新的一个补丁版本(新功能引入的缺陷都在其后的补丁里修掉了,所以同一个大版本内直接用最新补丁即可;旧补丁仍可继续用,插件不破坏既有接口)。
|
|
58
68
|
|
|
69
|
+
0.10.4 起只声明 0.2.1-rc.2 及以上:更早的预发布版本不再支持,它们已无法打开当前版本写下的会话,插件在那里的行为无从验证。
|
|
70
|
+
|
|
59
71
|
## 安装
|
|
60
72
|
|
|
61
73
|
通过 DSH CLI 安装:
|
package/icon.svg
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
2
|
<rect x="2.5" y="2.5" width="31" height="31" rx="9" fill="url(#tocbg)" />
|
|
3
|
-
<rect x="6" y="14
|
|
4
|
-
<rect x="6" y="14
|
|
5
|
-
<rect x="11" y="
|
|
3
|
+
<rect x="6" y="14" width="24" height="8" rx="4" fill="#ffffff" fill-opacity="0.22" />
|
|
4
|
+
<rect x="6" y="14" width="3" height="8" rx="1.5" fill="#1D4ED8" />
|
|
5
|
+
<rect x="11" y="8.7" width="15" height="3.2" rx="1.6" fill="#FF7AB8" />
|
|
6
6
|
<rect x="11" y="16.4" width="11.5" height="3.2" rx="1.6" fill="#FFB020" />
|
|
7
|
-
<rect x="11" y="
|
|
7
|
+
<rect x="11" y="24.1" width="7.5" height="3.2" rx="1.6" fill="#2DD4BF" />
|
|
8
8
|
<defs>
|
|
9
9
|
<linearGradient id="tocbg" x1="2.5" y1="2.5" x2="33.5" y2="33.5" gradientUnits="userSpaceOnUse">
|
|
10
10
|
<stop stop-color="#6EA5FF" />
|
package/lib/client.js
CHANGED
|
@@ -202,6 +202,13 @@ window.__ModuleLoader__.load({
|
|
|
202
202
|
"settings.dock": "默认停靠边缘",
|
|
203
203
|
"settings.dock.left": "左侧",
|
|
204
204
|
"settings.dock.right": "右侧",
|
|
205
|
+
"settings.surface": "视图",
|
|
206
|
+
"settings.surface.both": "面板 + 幕布",
|
|
207
|
+
"settings.surface.panel": "仅面板",
|
|
208
|
+
"settings.surface.curtain": "仅幕布",
|
|
209
|
+
"settings.scrollbar": "位置指示",
|
|
210
|
+
"settings.scrollbar.bar": "竖条",
|
|
211
|
+
"settings.scrollbar.band": "淡框",
|
|
205
212
|
"settings.levels": "显示的标题层级",
|
|
206
213
|
"settings.density": "大纲密度",
|
|
207
214
|
"settings.density.comfy": "标准",
|
|
@@ -230,7 +237,6 @@ window.__ModuleLoader__.load({
|
|
|
230
237
|
"settings.fuzzy": "模糊搜索",
|
|
231
238
|
"settings.hover": "悬停预览卡片",
|
|
232
239
|
"settings.remember": "记住阅读位置",
|
|
233
|
-
"settings.autoLoad": "自动加载历史",
|
|
234
240
|
"settings.debug": "在控制台打印诊断日志",
|
|
235
241
|
"settings.debug.tip": "开启后控制台会打印一行挂载诊断(宿主能力与跳转加载器状态)",
|
|
236
242
|
"settings.overridden": "已自定义",
|
|
@@ -334,6 +340,13 @@ window.__ModuleLoader__.load({
|
|
|
334
340
|
"settings.dock": "Default docked edge",
|
|
335
341
|
"settings.dock.left": "Left",
|
|
336
342
|
"settings.dock.right": "Right",
|
|
343
|
+
"settings.surface": "Views",
|
|
344
|
+
"settings.surface.both": "Panel + curtain",
|
|
345
|
+
"settings.surface.panel": "Panel only",
|
|
346
|
+
"settings.surface.curtain": "Curtain only",
|
|
347
|
+
"settings.scrollbar": "Position marker",
|
|
348
|
+
"settings.scrollbar.bar": "Bar",
|
|
349
|
+
"settings.scrollbar.band": "Band",
|
|
337
350
|
"settings.levels": "Heading levels shown",
|
|
338
351
|
"settings.density": "Outline density",
|
|
339
352
|
"settings.density.comfy": "Comfy",
|
|
@@ -362,7 +375,6 @@ window.__ModuleLoader__.load({
|
|
|
362
375
|
"settings.fuzzy": "Fuzzy search",
|
|
363
376
|
"settings.hover": "Hover preview card",
|
|
364
377
|
"settings.remember": "Remember the reading position",
|
|
365
|
-
"settings.autoLoad": "Load history automatically",
|
|
366
378
|
"settings.debug": "Print the mount diagnostic to the console",
|
|
367
379
|
"settings.debug.tip": "The console then prints one mount line (host capabilities and jump-loader state)",
|
|
368
380
|
"settings.overridden": "Customized",
|
|
@@ -435,11 +447,11 @@ window.__ModuleLoader__.load({
|
|
|
435
447
|
// purpose — they describe THIS screen, not the account.
|
|
436
448
|
var NAMESPACE = "dsh-quick-toc";
|
|
437
449
|
// Fields backed by the Host settings document; everything else is localStorage.
|
|
438
|
-
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 };
|
|
450
|
+
var HOST_FIELDS = { dock: 1, surface: 1, scrollbar: 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 };
|
|
439
451
|
// The schema defaults (mirrored here for the card's per-field reset and for the
|
|
440
452
|
// one-time import of a 0.5.x install's stored values).
|
|
441
453
|
var DEFAULTS = {
|
|
442
|
-
dock: "left", y: 0, w: 0, h: 0,
|
|
454
|
+
dock: "left", surface: "both", scrollbar: "bar", y: 0, w: 0, h: 0,
|
|
443
455
|
// "share" of see-through: 0 = nearly solid, 100 = the lightest material
|
|
444
456
|
levels: [1, 2, 3, 4, 5, 6], density: "comfy", accent: "blue",
|
|
445
457
|
glass: true, glassStrength: 60, sheetHeight: 1,
|
|
@@ -558,10 +570,24 @@ window.__ModuleLoader__.load({
|
|
|
558
570
|
// colour, and the blur strings become undefined rather than empty.
|
|
559
571
|
// A device that cannot hold the frame rate loses the blur by itself. The
|
|
560
572
|
// verdict is remembered, so a slow machine pays for the probe once.
|
|
561
|
-
|
|
573
|
+
//
|
|
574
|
+
// v2: the v1 key was decided while the curtain's 0.5s ENTRANCE was being sampled,
|
|
575
|
+
// and the entrance composites the blurred sheet every frame by design. Measured in
|
|
576
|
+
// a sandbox profile with a clean store: the v1 verdict appeared after a curtain
|
|
577
|
+
// opening and never went away, which silently removed the blur from BOTH surfaces
|
|
578
|
+
// for good (the palette is shared). The material is now judged on the settled
|
|
579
|
+
// surface (see the probe), and the switch is a real way back (see the card):
|
|
580
|
+
// turning it on clears an automatic verdict. A new key lets a browser that already
|
|
581
|
+
// carries the old one start clean.
|
|
582
|
+
var AUTO_OFF_KEY = "dsh-quick-toc.glassAutoOff.v2";
|
|
562
583
|
var autoGlassOff = (function () {
|
|
563
584
|
try { return localStorage.getItem(AUTO_OFF_KEY) === "1"; } catch (e) { return false; }
|
|
564
585
|
})();
|
|
586
|
+
// The reader asking for the material again outranks a measurement made for them.
|
|
587
|
+
var clearAutoGlassOff = function () {
|
|
588
|
+
autoGlassOff = false;
|
|
589
|
+
try { localStorage.removeItem(AUTO_OFF_KEY); } catch (e) {}
|
|
590
|
+
};
|
|
565
591
|
var glassC = function (base, prefs) {
|
|
566
592
|
var on = !!(prefs && prefs.glass) && !autoGlassOff && GLASS_OK;
|
|
567
593
|
var s = prefs && prefs.glassStrength !== undefined ? prefs.glassStrength : 60;
|
|
@@ -587,6 +613,37 @@ window.__ModuleLoader__.load({
|
|
|
587
613
|
out.sheetBlur = on ? "blur(5px) saturate(1.1)" : undefined;
|
|
588
614
|
return out;
|
|
589
615
|
};
|
|
616
|
+
// The ruler is a TAPE: every heading gets a slot at least `minGap` apart, and when
|
|
617
|
+
// the slots no longer fit the track the tape is TALLER than its window and slides
|
|
618
|
+
// with the scroll. This is the behaviour the reader asked for over a static map:
|
|
619
|
+
// the bars have to move as the outline scrolls, and a dense stretch has to look
|
|
620
|
+
// dense. A slot therefore does NOT claim the row's place in the content any more —
|
|
621
|
+
// the highlight is the slice of the tape the viewport mark covers, so the bars that
|
|
622
|
+
// light up always sit inside the mark and travel through it as the list scrolls.
|
|
623
|
+
// count headings on the tape; track usable lane height
|
|
624
|
+
// scrollTop/view/total the outline's viewport and content
|
|
625
|
+
function rulerLayout(count, total, track, scrollTop, view, minGap) {
|
|
626
|
+
var gap = count > 1 ? Math.max(minGap, track / (count - 1)) : 0;
|
|
627
|
+
var tapeH = Math.max(track, Math.round((count - 1) * gap) + 10);
|
|
628
|
+
var shift = Math.max(0, tapeH - track);
|
|
629
|
+
var room = Math.max(1, total - view);
|
|
630
|
+
var pos = Math.max(0, Math.min(1, scrollTop / room));
|
|
631
|
+
var share = total > 0 ? Math.min(1, view / total) : 1;
|
|
632
|
+
var markH = Math.max(28, Math.round(track * share));
|
|
633
|
+
var markTop = Math.max(0, Math.min(track - markH, Math.round((track - markH) * pos)));
|
|
634
|
+
var offset = shift * pos;
|
|
635
|
+
var canScroll = total - view > 4;
|
|
636
|
+
var ticks = [];
|
|
637
|
+
for (var i = 0; i < count; i++) {
|
|
638
|
+
var y = Math.round(i * gap) - offset;
|
|
639
|
+
ticks.push({
|
|
640
|
+
top: Math.round(i * gap),
|
|
641
|
+
// a 10px box: a bar whose centre is inside the mark counts as inside it
|
|
642
|
+
inView: canScroll && y >= markTop - 5 && y <= markTop + markH + 5
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
return { ticks: ticks, gap: gap, tapeH: tapeH, shift: shift, offset: offset };
|
|
646
|
+
}
|
|
590
647
|
// Writing a field's schema default through the card IS its reset — a value that
|
|
591
648
|
// equals the default carries no customization, so it must not pin an entry (and
|
|
592
649
|
// the "customized" badge with it) into the user layer.
|
|
@@ -854,6 +911,9 @@ window.__ModuleLoader__.load({
|
|
|
854
911
|
var handleNum = Number(v.handle);
|
|
855
912
|
return {
|
|
856
913
|
dock: v.dock === "right" ? "right" : "left",
|
|
914
|
+
// which views exist; anything unknown falls back to the historical "both"
|
|
915
|
+
surface: v.surface === "panel" || v.surface === "curtain" ? v.surface : "both",
|
|
916
|
+
scrollbar: v.scrollbar === "band" ? "band" : "bar",
|
|
857
917
|
lang: v.lang === "zh" || v.lang === "en" ? v.lang : "auto",
|
|
858
918
|
levels: levels || LEVELS_ALL.slice(),
|
|
859
919
|
density: v.density === "compact" ? "compact" : "comfy",
|
|
@@ -876,7 +936,7 @@ window.__ModuleLoader__.load({
|
|
|
876
936
|
var over = hostOverlay(hostSnap);
|
|
877
937
|
if (!over) return local;
|
|
878
938
|
return {
|
|
879
|
-
dock: over.dock, y: local.y, w: local.w, h: local.h,
|
|
939
|
+
dock: over.dock, surface: over.surface, scrollbar: over.scrollbar, y: local.y, w: local.w, h: local.h,
|
|
880
940
|
levels: over.levels, density: over.density, accent: over.accent,
|
|
881
941
|
glass: over.glass, glassStrength: over.glassStrength, sheetHeight: over.sheetHeight,
|
|
882
942
|
zoom: over.zoom, sheetZoom: over.sheetZoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
|
|
@@ -2653,6 +2713,14 @@ window.__ModuleLoader__.load({
|
|
|
2653
2713
|
programScrollRef.current = Date.now() + (ms || 250);
|
|
2654
2714
|
};
|
|
2655
2715
|
var lastScrollTopRef = react.useRef(0);
|
|
2716
|
+
// One page per ARRIVAL at the top: set when that page was asked for (by the
|
|
2717
|
+
// scroll's rising edge or by a wheel tick) and cleared once the list leaves the
|
|
2718
|
+
// top. Sitting at the top used to ask again on every wheel tick.
|
|
2719
|
+
var topLoadRef = react.useRef(false);
|
|
2720
|
+
// Set when the top asked the host for a page: the reveal waits for that page to
|
|
2721
|
+
// land instead of showing rows whose history is not here yet (see the effect
|
|
2722
|
+
// below the `groups` memo).
|
|
2723
|
+
var pendingRevealRef = react.useRef(false);
|
|
2656
2724
|
var lastHostClickRef = react.useRef(0); // throttle for the "load earlier" click
|
|
2657
2725
|
// Auto-load bookkeeping (see the paging block): at most one pull in flight, a beat
|
|
2658
2726
|
// between pulls, and a per-gesture budget so a runaway loop can never swallow the
|
|
@@ -3488,6 +3556,20 @@ window.__ModuleLoader__.load({
|
|
|
3488
3556
|
// two comparisons disagree forever — the panel then republishes its default on
|
|
3489
3557
|
// every pass and stomps a fresh change with the value it had just read back.
|
|
3490
3558
|
var prefs = usePrefs();
|
|
3559
|
+
// ---- which surfaces exist ------------------------------------------------
|
|
3560
|
+
// 'both' is the historical behaviour. The other two do not merely prefer one
|
|
3561
|
+
// surface: they REMOVE the other, which is what was asked for (readers who only
|
|
3562
|
+
// ever use the curtain kept getting the docked panel's edge pill over the
|
|
3563
|
+
// transcript, and readers who only use the panel never wanted the droplet).
|
|
3564
|
+
var surface = prefs.surface === "curtain" ? "curtain" : (prefs.surface === "panel" ? "panel" : "both");
|
|
3565
|
+
var panelMode = surface !== "curtain";
|
|
3566
|
+
var curtainMode = surface !== "panel";
|
|
3567
|
+
// A preference change (or a document written elsewhere) must not leave the
|
|
3568
|
+
// removed surface on screen: close what is no longer offered.
|
|
3569
|
+
react.useEffect(function () {
|
|
3570
|
+
if (!curtainMode && sheetOpen) closeSheet();
|
|
3571
|
+
if (!panelMode && open) setOpen(false);
|
|
3572
|
+
}, [surface, sheetOpen, open]);
|
|
3491
3573
|
// ---- the day strip (see its call site) -----------------------------------
|
|
3492
3574
|
// Declared up here with the other hooks: an early "nothing to show yet"
|
|
3493
3575
|
// return must not skip them, or the first empty render and every real one
|
|
@@ -3544,9 +3626,11 @@ window.__ModuleLoader__.load({
|
|
|
3544
3626
|
measure();
|
|
3545
3627
|
return undefined;
|
|
3546
3628
|
}, [open, sheetOpen]);
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3629
|
+
// A revision, not a value: the probe's verdict must re-render the panel, while
|
|
3630
|
+
// the palette itself reads the module flag (so the card can clear it and the
|
|
3631
|
+
// next render — any preference write — picks the material back up).
|
|
3632
|
+
var _sGlassRev = react.useState(0);
|
|
3633
|
+
var bumpGlassRev = _sGlassRev[1];
|
|
3550
3634
|
var _sSheetFrac = react.useState(null);
|
|
3551
3635
|
var sheetDragFrac = _sSheetFrac[0];
|
|
3552
3636
|
var setSheetDragFrac = _sSheetFrac[1];
|
|
@@ -3571,10 +3655,12 @@ window.__ModuleLoader__.load({
|
|
|
3571
3655
|
setSheetDragFrac(null);
|
|
3572
3656
|
};
|
|
3573
3657
|
// ---- slow-device probe (see AUTO_OFF_KEY) --------------------------------
|
|
3574
|
-
// Sampled once per curtain opening, and only while the material is on
|
|
3575
|
-
//
|
|
3576
|
-
//
|
|
3577
|
-
//
|
|
3658
|
+
// Sampled once per curtain opening, and only while the material is on. The
|
|
3659
|
+
// entrance is deliberately EXCLUDED: the 0.5s drop composites a blurred sheet
|
|
3660
|
+
// every frame by design, and sampling it is what switched the blur off in a
|
|
3661
|
+
// sandbox profile whose page then held the frame rate perfectly well. The
|
|
3662
|
+
// settled surface is what the reader actually lives with, so that is what gets
|
|
3663
|
+
// judged: 40 frames starting after the drop has landed.
|
|
3578
3664
|
react.useEffect(function () {
|
|
3579
3665
|
if (!sheetOpen || !prefs.glass || autoGlassOff) return undefined;
|
|
3580
3666
|
if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") return undefined;
|
|
@@ -3596,19 +3682,24 @@ window.__ModuleLoader__.load({
|
|
|
3596
3682
|
if (p90 > 26) {
|
|
3597
3683
|
autoGlassOff = true;
|
|
3598
3684
|
try { localStorage.setItem(AUTO_OFF_KEY, "1"); } catch (e) {}
|
|
3599
|
-
|
|
3685
|
+
bumpGlassRev(function (n) { return n + 1; });
|
|
3600
3686
|
}
|
|
3601
3687
|
};
|
|
3602
|
-
|
|
3603
|
-
var
|
|
3604
|
-
|
|
3605
|
-
|
|
3688
|
+
// 620ms = the 500ms entrance plus a margin for its own landing frame
|
|
3689
|
+
var start = setTimeout(function () {
|
|
3690
|
+
if (stop) return;
|
|
3691
|
+
last = 0;
|
|
3692
|
+
window.requestAnimationFrame(tick);
|
|
3693
|
+
}, 620);
|
|
3694
|
+
var guard = setTimeout(finish, 620 + 1200);
|
|
3695
|
+
return function () { stop = true; clearTimeout(start); clearTimeout(guard); };
|
|
3696
|
+
}, [sheetOpen, prefs.glass]);
|
|
3606
3697
|
// The render's own palette: blue shares the module object itself (default
|
|
3607
3698
|
// path: zero behaviour change), any other theme merges the same keys.
|
|
3608
3699
|
// Every C.* use below this line — rows, chips, the accent bar, hit marks —
|
|
3609
3700
|
// follows the preference without touching a single call site.
|
|
3610
3701
|
var C = glassC(themeC(prefs.accent), {
|
|
3611
|
-
glass: prefs.glass && !
|
|
3702
|
+
glass: prefs.glass && !autoGlassOff,
|
|
3612
3703
|
glassStrength: prefs.glassStrength
|
|
3613
3704
|
});
|
|
3614
3705
|
// the once-injected stylesheet cannot see per-render values: repaint its
|
|
@@ -4119,8 +4210,8 @@ window.__ModuleLoader__.load({
|
|
|
4119
4210
|
autoLoadStep(el);
|
|
4120
4211
|
}, fast ? AUTO_WAIT_MS : AUTO_PACE_MS);
|
|
4121
4212
|
};
|
|
4122
|
-
var autoLoadStep = function (el) {
|
|
4123
|
-
if (!autoLoadRef.current || !el || !el.isConnected) return false;
|
|
4213
|
+
var autoLoadStep = function (el, force) {
|
|
4214
|
+
if ((!autoLoadRef.current && !force) || !el || !el.isConnected) return false;
|
|
4124
4215
|
if (pullBusyRef.current) return true; // a pull is in flight: still progressing
|
|
4125
4216
|
var now = Date.now();
|
|
4126
4217
|
if (now - autoStampRef.current > AUTO_BURST_IDLE_MS) autoBurstRef.current = 0;
|
|
@@ -4180,19 +4271,24 @@ window.__ModuleLoader__.load({
|
|
|
4180
4271
|
};
|
|
4181
4272
|
var loadOlderOutline = function (el) {
|
|
4182
4273
|
var grow = function () {
|
|
4183
|
-
if (visibleCount
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4274
|
+
if (visibleCount >= groups.length) return false;
|
|
4275
|
+
// Reveal only rows that are already LOADED. A placeholder would render as
|
|
4276
|
+
// 未加载, and the reader's rule for this gesture is "one page, like the host's
|
|
4277
|
+
// own 加载更早" — the index must not run ahead of the history behind it.
|
|
4278
|
+
var top = groups.length - visibleCount;
|
|
4279
|
+
var run = 0;
|
|
4280
|
+
while (run < PAGE_SIZE && top - 1 - run >= 0 && groups[top - 1 - run] && !groups[top - 1 - run].ghost) run++;
|
|
4281
|
+
if (!run) return false;
|
|
4282
|
+
// Revealing older rows prepends them, so the reader's content is pushed down.
|
|
4283
|
+
// Sample first and let the layout effect take the push out again in the same
|
|
4284
|
+
// frame: correcting two frames later (the old double-rAF) IS the bounce the
|
|
4285
|
+
// reader saw when they reached the unloaded part of the list.
|
|
4286
|
+
holdPlace(el);
|
|
4287
|
+
setVisibleCount(Math.min(groups.length, visibleCount + run));
|
|
4288
|
+
// the revealed rows are not in the DOM yet, so re-check once after the render:
|
|
4289
|
+
// if the reveal uncovered unloaded turns, they load within the same gesture
|
|
4290
|
+
autoRetry();
|
|
4291
|
+
return true;
|
|
4196
4292
|
};
|
|
4197
4293
|
var viaHostButton = function () { return viaHostButtonRef.current(); };
|
|
4198
4294
|
// In SEARCH mode the list shows matches, not the outline: paging the outline's
|
|
@@ -4206,10 +4302,17 @@ window.__ModuleLoader__.load({
|
|
|
4206
4302
|
if (pulled) confirmPlace(el);
|
|
4207
4303
|
return pulled || grow();
|
|
4208
4304
|
}
|
|
4209
|
-
// Outline mode: reveal
|
|
4210
|
-
//
|
|
4305
|
+
// Outline mode: reveal the next slice of the index and pull exactly ONE host
|
|
4306
|
+
// page — the same `loadOlder()` the host's own 加载更早 button calls. This used
|
|
4307
|
+
// to run the auto-loader here, whose "cover the ghost you reached" path pages up
|
|
4308
|
+
// to 200 messages a hop: one wheel arrival could therefore load several times
|
|
4309
|
+
// what the button loads and leave the outline full of 未加载 rows.
|
|
4211
4310
|
var grew = grow();
|
|
4212
|
-
var fetched =
|
|
4311
|
+
var fetched = pullPage() || viaHostButton();
|
|
4312
|
+
// Nothing loaded above the window (the frontier): wait for the page to land and
|
|
4313
|
+
// reveal its turns then. When the index COULD advance, the jump above already
|
|
4314
|
+
// moved the reader off the top, which re-arms the gesture by itself.
|
|
4315
|
+
if (fetched && !grew) pendingRevealRef.current = true;
|
|
4213
4316
|
return grew || fetched;
|
|
4214
4317
|
};
|
|
4215
4318
|
|
|
@@ -4249,7 +4352,13 @@ window.__ModuleLoader__.load({
|
|
|
4249
4352
|
}
|
|
4250
4353
|
if (hint === "more") setHint(""); // scrolling up is what the hint asked for
|
|
4251
4354
|
if (el.scrollTop <= 1) {
|
|
4252
|
-
|
|
4355
|
+
// ONE page per arrival, exactly like the host's own 加载更多: a spin used to
|
|
4356
|
+
// reveal another PAGE_SIZE of index rows on EVERY tick, so one gesture could
|
|
4357
|
+
// uncover far more turns than were loaded and leave 未加载 rows behind. The
|
|
4358
|
+
// flag is cleared when the list leaves the top (see onListScroll).
|
|
4359
|
+
var alreadyLoaded = topLoadRef.current;
|
|
4360
|
+
topLoadRef.current = true;
|
|
4361
|
+
var progressed = alreadyLoaded ? false : loadOlderOutline(el);
|
|
4253
4362
|
if (!progressed) {
|
|
4254
4363
|
if (query.trim() !== "") {
|
|
4255
4364
|
if (hint !== "oldest") setHint("oldest");
|
|
@@ -4267,6 +4376,165 @@ window.__ModuleLoader__.load({
|
|
|
4267
4376
|
};
|
|
4268
4377
|
|
|
4269
4378
|
// scroll to the top edge also loads older groups (keeps the visual position)
|
|
4379
|
+
// ---- the scroll-position indicator ---------------------------------------
|
|
4380
|
+
// A density ruler plus a viewport marker on the list's right edge.
|
|
4381
|
+
// * the TICKS are a TAPE: one slot per H1/H2, at least 7px apart, and when the
|
|
4382
|
+
// slots overflow the window the tape slides with the scroll (see
|
|
4383
|
+
// rulerLayout). Clicking one jumps to its heading.
|
|
4384
|
+
// * the MARKER is where the reader is: a short vertical bar ("bar") or a soft
|
|
4385
|
+
// band ("band"). Its height is the viewport's share of the content; the bars
|
|
4386
|
+
// under it are the ones painted in the theme colour.
|
|
4387
|
+
// It must live OUTSIDE the scroller: the list carries a top/bottom fade mask,
|
|
4388
|
+
// and anything drawn inside it is eaten at the ends (the "clipped, odd" look).
|
|
4389
|
+
var barHostRef = react.useRef(null);
|
|
4390
|
+
var barThumbRef = react.useRef(null);
|
|
4391
|
+
var barTapeRef = react.useRef(null);
|
|
4392
|
+
var barDragRef = react.useRef(null);
|
|
4393
|
+
var _sBarHot = react.useState(false);
|
|
4394
|
+
var barHot = _sBarHot[0];
|
|
4395
|
+
var setBarHot = _sBarHot[1];
|
|
4396
|
+
// The bottom needs room for the panel's 12px corner AND the marker's 5px glow,
|
|
4397
|
+
// or the end of the mark gets shaved off exactly when it reaches the bottom.
|
|
4398
|
+
var BAR_BOTTOM_GAP = 20;
|
|
4399
|
+
var BAR_INSET_TOP = 10;
|
|
4400
|
+
var TICK_MIN_GAP = 7; // the ruler stops getting denser than this
|
|
4401
|
+
var barMode = prefs.scrollbar === "band" ? "band" : "bar";
|
|
4402
|
+
// the marker lives in its own lane, never on top of the rows: the list keeps
|
|
4403
|
+
// this much padding on its right so text and the current-group box stop short
|
|
4404
|
+
// The curtain has room, so the whole ruler scales up with the marker: the lane
|
|
4405
|
+
// widens, the ticks get longer and the frame grows with them.
|
|
4406
|
+
var barScale = sheetOpen ? 1.6 : 1;
|
|
4407
|
+
var barTickW = Math.round(10 * barScale);
|
|
4408
|
+
var barLane = (barMode === "band" ? 22 : 24) + (sheetOpen ? 8 : 0);
|
|
4409
|
+
// The air between the list's content edge and the lane. The current-group box
|
|
4410
|
+
// bleeds 5px into it (its own -5px side margin), so the box the reader sees sits
|
|
4411
|
+
// ~13px from the band — two tinted frames needed room, they were nearly touching.
|
|
4412
|
+
var BAR_TEXT_GAP = 18;
|
|
4413
|
+
// where the marker currently sits, written straight to the DOM: a re-render per
|
|
4414
|
+
// scroll frame would rebuild the whole outline
|
|
4415
|
+
// One pass over the scroll position: the tape (the ruler) slides, and the marker
|
|
4416
|
+
// (a rounded bar, or the band that wraps the ruler) follows.
|
|
4417
|
+
var layoutScrollBar = function (el) {
|
|
4418
|
+
if (typeof layoutBarLane === "function") layoutBarLane();
|
|
4419
|
+
var host = barHostRef.current;
|
|
4420
|
+
var thumb = barThumbRef.current;
|
|
4421
|
+
var tape = barTapeRef.current;
|
|
4422
|
+
if (!host || !el) return;
|
|
4423
|
+
var view = el.clientHeight || 0;
|
|
4424
|
+
var total = el.scrollHeight || 0;
|
|
4425
|
+
var laneH = host.clientHeight || 0;
|
|
4426
|
+
var track = Math.max(40, laneH - BAR_INSET_TOP - BAR_BOTTOM_GAP);
|
|
4427
|
+
var share = total > 0 ? Math.min(1, view / total) : 1;
|
|
4428
|
+
var scrollTop = el.scrollTop || 0;
|
|
4429
|
+
var room = Math.max(1, total - view);
|
|
4430
|
+
var pos = Math.max(0, Math.min(1, scrollTop / room));
|
|
4431
|
+
var canScroll = total - view > 4;
|
|
4432
|
+
// The ruler: one slot per heading, at least TICK_MIN_GAP apart. When the slots
|
|
4433
|
+
// overflow the window the tape is taller and slides with the scroll — the bars
|
|
4434
|
+
// move as the outline moves, and a dense stretch stays dense.
|
|
4435
|
+
if (tape && tape.style) {
|
|
4436
|
+
var kids = tape.children || [];
|
|
4437
|
+
var laid = rulerLayout(kids.length, total, track, scrollTop, view, TICK_MIN_GAP);
|
|
4438
|
+
for (var ti = 0; ti < kids.length; ti++) {
|
|
4439
|
+
var kid = kids[ti];
|
|
4440
|
+
var g = laid.ticks[ti];
|
|
4441
|
+
if (!kid || !kid.style || !g) continue;
|
|
4442
|
+
kid.style.display = "";
|
|
4443
|
+
kid.style.top = g.top + "px"; // the slot; the 10px box centres its bar 5px in
|
|
4444
|
+
var bar = kid.firstElementChild || (kid.children && kid.children[0]);
|
|
4445
|
+
if (bar && bar.style) {
|
|
4446
|
+
bar.style.background = g.inView ? C.accent : "rgba(128,128,128,0.32)";
|
|
4447
|
+
bar.style.opacity = g.inView ? "0.95" : "0.8";
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
tape.style.height = laid.tapeH + "px";
|
|
4451
|
+
tape.style.transform = "translateY(" + Math.round(-laid.offset) + "px)";
|
|
4452
|
+
}
|
|
4453
|
+
if (!thumb || track <= 8) return;
|
|
4454
|
+
// The mark is the viewport's share of the content: its height is that share and
|
|
4455
|
+
// its travel is the scroll's. (With share*track < the 28px grab floor the mark
|
|
4456
|
+
// keeps its grab size and the bottom end gives way.)
|
|
4457
|
+
var markHeight = Math.max(28, Math.round(track * share));
|
|
4458
|
+
var markTop = Math.max(0, Math.min(track - markHeight, Math.round((track - markHeight) * pos)));
|
|
4459
|
+
thumb.style.height = markHeight + "px";
|
|
4460
|
+
// the element's own top already carries BAR_INSET_TOP (see its style), so the
|
|
4461
|
+
// transform adds only the travel — one inset, or the mark sits 10px low
|
|
4462
|
+
thumb.style.transform = "translateY(" + markTop + "px)";
|
|
4463
|
+
thumb.style.opacity = canScroll ? "1" : "0.3";
|
|
4464
|
+
thumb.style.pointerEvents = canScroll ? "auto" : "none";
|
|
4465
|
+
};
|
|
4466
|
+
// The lane is an in-flow flex column beside the list, so there is nothing to
|
|
4467
|
+
// position by hand: this stays as a no-op because layoutScrollBar calls it.
|
|
4468
|
+
var layoutBarLane = function () {};
|
|
4469
|
+
var onBarDown = function (e) {
|
|
4470
|
+
var el = listRef.current;
|
|
4471
|
+
if (!el) return;
|
|
4472
|
+
e.preventDefault();
|
|
4473
|
+
e.stopPropagation();
|
|
4474
|
+
// A drag is measured in CONTENT space, fixed at the moment it starts: the
|
|
4475
|
+
// pointer's travel maps to the content that existed then, so history arriving
|
|
4476
|
+
// underneath can never accelerate the reader towards the top.
|
|
4477
|
+
var host0 = barHostRef.current;
|
|
4478
|
+
var track0 = Math.max(1, (host0 ? host0.clientHeight : 0) - BAR_INSET_TOP - BAR_BOTTOM_GAP);
|
|
4479
|
+
var thumbH0 = barThumbRef.current ? barThumbRef.current.offsetHeight || 22 : 22;
|
|
4480
|
+
barDragRef.current = {
|
|
4481
|
+
moved: false,
|
|
4482
|
+
startY: e.clientY,
|
|
4483
|
+
startTop: el.scrollTop || 0,
|
|
4484
|
+
room0: Math.max(1, el.scrollHeight - el.clientHeight),
|
|
4485
|
+
travel0: Math.max(1, track0 - thumbH0)
|
|
4486
|
+
};
|
|
4487
|
+
setBarHot(true);
|
|
4488
|
+
if (e.currentTarget && e.pointerId !== undefined && e.currentTarget.setPointerCapture) {
|
|
4489
|
+
try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* no capture: fine */ }
|
|
4490
|
+
}
|
|
4491
|
+
};
|
|
4492
|
+
var onBarMove = function (e) {
|
|
4493
|
+
var d = barDragRef.current;
|
|
4494
|
+
var el = listRef.current;
|
|
4495
|
+
if (!d || !el) return;
|
|
4496
|
+
var dy = e.clientY - d.startY;
|
|
4497
|
+
if (Math.abs(dy) > 2) d.moved = true;
|
|
4498
|
+
// Reached the top: stand still until the page that arrival asked for lands.
|
|
4499
|
+
// The pointer turning back DOWN is an ordinary drag, though — keeping the hold
|
|
4500
|
+
// there latched the mark at the top until the button was released.
|
|
4501
|
+
if (d.hold) {
|
|
4502
|
+
if (e.clientY > d.holdY + 1) d.hold = false;
|
|
4503
|
+
else if (el.scrollHeight <= d.holdHeight) return;
|
|
4504
|
+
else d.hold = false;
|
|
4505
|
+
}
|
|
4506
|
+
var want = d.startTop + (dy / d.travel0) * d.room0;
|
|
4507
|
+
var room = Math.max(1, el.scrollHeight - el.clientHeight);
|
|
4508
|
+
var next = Math.max(0, Math.min(room, want));
|
|
4509
|
+
if (next <= 0 && dy < 0) {
|
|
4510
|
+
if (!d.hold) { d.hold = true; d.holdY = e.clientY; d.holdHeight = el.scrollHeight; }
|
|
4511
|
+
}
|
|
4512
|
+
el.scrollTop = Math.round(next);
|
|
4513
|
+
};
|
|
4514
|
+
var onBarUp = function (e) {
|
|
4515
|
+
var d = barDragRef.current;
|
|
4516
|
+
barDragRef.current = null;
|
|
4517
|
+
setBarHot(false);
|
|
4518
|
+
if (e && e.currentTarget && e.currentTarget.releasePointerCapture && e.pointerId !== undefined) {
|
|
4519
|
+
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ }
|
|
4520
|
+
}
|
|
4521
|
+
// a press that did not drag centres the view on the pointer instead
|
|
4522
|
+
if (d && !d.moved) {
|
|
4523
|
+
var el = listRef.current;
|
|
4524
|
+
var host = barHostRef.current;
|
|
4525
|
+
var thumb = barThumbRef.current;
|
|
4526
|
+
if (el && host && thumb) {
|
|
4527
|
+
var rect = host.getBoundingClientRect();
|
|
4528
|
+
// the same band the mark travels in (see layoutScrollBar): the lane's top
|
|
4529
|
+
// inset belongs to both ends, or a press lands the view a hair off
|
|
4530
|
+
var track = Math.max(1, (host.clientHeight || 0) - BAR_INSET_TOP - BAR_BOTTOM_GAP);
|
|
4531
|
+
var thumbH = thumb.offsetHeight || 22;
|
|
4532
|
+
var y = Math.max(0, Math.min(track - thumbH, e.clientY - rect.top - BAR_INSET_TOP - thumbH / 2));
|
|
4533
|
+
var room = Math.max(1, el.scrollHeight - el.clientHeight);
|
|
4534
|
+
el.scrollTop = Math.round((y / Math.max(1, track - thumbH)) * room);
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
};
|
|
4270
4538
|
var onListScroll = function (e) {
|
|
4271
4539
|
// A scroll the panel caused itself (see markProgramScroll) carries no
|
|
4272
4540
|
// reader intent: it still updates the bottom state and the paging anchor,
|
|
@@ -4283,7 +4551,12 @@ window.__ModuleLoader__.load({
|
|
|
4283
4551
|
if (hint === "more") setHint("");
|
|
4284
4552
|
if (hoverCard) hoverEnd(true);
|
|
4285
4553
|
}
|
|
4554
|
+
var wasAbove24 = lastScrollTopRef.current > 24;
|
|
4286
4555
|
lastScrollTopRef.current = el.scrollTop;
|
|
4556
|
+
// leaving the top re-arms the one-page gate (see onListWheel): a later
|
|
4557
|
+
// arrival is a new request, staying here is not
|
|
4558
|
+
if (el.scrollTop > 24) topLoadRef.current = false;
|
|
4559
|
+
layoutScrollBar(el);
|
|
4287
4560
|
if (!programmatic) syncBackBtn(el); // reached the remembered place by hand? the button is done
|
|
4288
4561
|
// Remember what the reader is looking at. Sampling on every scroll event is what
|
|
4289
4562
|
// lets a change the reader did NOT start — the host extending the window by itself
|
|
@@ -4294,22 +4567,21 @@ window.__ModuleLoader__.load({
|
|
|
4294
4567
|
// trigger any more: on any upward movement, ask the auto-loader whether the
|
|
4295
4568
|
// unloaded stretch is coming into range (it decides, see frontierApproaching).
|
|
4296
4569
|
// Throttled, because that probe reads geometry.
|
|
4297
|
-
|
|
4298
|
-
autoProbeRef.current = Date.now();
|
|
4299
|
-
autoLoadStep(el);
|
|
4300
|
-
}
|
|
4570
|
+
// no automatic paging any more: the reader asks by reaching the top
|
|
4301
4571
|
if (el.scrollTop <= 24) {
|
|
4302
4572
|
// Reaching the top is the ONLY place the exhausted case can be told
|
|
4303
4573
|
// apart from "keep scrolling": after a search the list is parked at the
|
|
4304
4574
|
// bottom, so the first wheel-up merely scrolls and never gets here.
|
|
4305
|
-
var
|
|
4575
|
+
var atTopFresh = wasAbove24 && !barDragRef.current && (!programmatic || query.trim() !== "");
|
|
4576
|
+
// the outline window grows only on that fresh arrival too: sitting at the top used
|
|
4577
|
+
// to keep revealing ghost pages, which looked like a flood of 未加载 rows
|
|
4578
|
+
if (atTopFresh) topLoadRef.current = true;
|
|
4579
|
+
var progressed = atTopFresh ? loadOlderOutline(el) : false;
|
|
4306
4580
|
if (!progressed && upward) {
|
|
4307
4581
|
if (query.trim() !== "") {
|
|
4308
4582
|
if (hint !== "oldest") setHint("oldest");
|
|
4309
4583
|
} else if (atHistoryStart()) {
|
|
4310
4584
|
showBanner(T("search.hintOldest"));
|
|
4311
|
-
} else if (!prefs.autoLoad) {
|
|
4312
|
-
showBanner(T("hint.autoOff"));
|
|
4313
4585
|
}
|
|
4314
4586
|
}
|
|
4315
4587
|
}
|
|
@@ -4570,6 +4842,20 @@ window.__ModuleLoader__.load({
|
|
|
4570
4842
|
// render (the "did that turn actually page in?" verdict below).
|
|
4571
4843
|
var groupsLiveRef = react.useRef(groups);
|
|
4572
4844
|
groupsLiveRef.current = groups;
|
|
4845
|
+
// A top arrival asked the host for one page: when that page LANDs, extend the
|
|
4846
|
+
// window over the turns that just became real (never over a placeholder). The
|
|
4847
|
+
// reader sees exactly what loaded, and the wheel does not run ahead of the data.
|
|
4848
|
+
react.useEffect(function () {
|
|
4849
|
+
if (!pendingRevealRef.current) return;
|
|
4850
|
+
var top = groups.length - visibleCount;
|
|
4851
|
+
var run = 0;
|
|
4852
|
+
while (run < PAGE_SIZE && top - 1 - run >= 0 && groups[top - 1 - run] && !groups[top - 1 - run].ghost) run++;
|
|
4853
|
+
if (!run) return; // the page has not landed yet
|
|
4854
|
+
pendingRevealRef.current = false;
|
|
4855
|
+
var el = listRef.current;
|
|
4856
|
+
if (el) holdPlace(el);
|
|
4857
|
+
setVisibleCount(Math.min(groups.length, visibleCount + run));
|
|
4858
|
+
}, [groups, visibleCount]);
|
|
4573
4859
|
react.useEffect(function () {
|
|
4574
4860
|
return function () {
|
|
4575
4861
|
if (keepPlaceRafRef.current) cancelAnimationFrame(keepPlaceRafRef.current);
|
|
@@ -4636,6 +4922,189 @@ window.__ModuleLoader__.load({
|
|
|
4636
4922
|
var shownGroups = groups.slice(Math.max(0, groups.length - visibleCount));
|
|
4637
4923
|
var shownTrees = groupTrees.slice(groupTrees.length - shownGroups.length);
|
|
4638
4924
|
|
|
4925
|
+
// the ticks: one per H1/H2/H3 entry the list actually renders. The window
|
|
4926
|
+
// starts at the first shown group, so the ruler never claims a place for a
|
|
4927
|
+
// row that is not in the DOM (the reader can see every tick it draws).
|
|
4928
|
+
var barFirstGi = Math.max(0, groups.length - visibleCount);
|
|
4929
|
+
var barTicks = react.useMemo(function () {
|
|
4930
|
+
var all = [];
|
|
4931
|
+
for (var k = 0; k < shownGroups.length; k++) {
|
|
4932
|
+
var gi = barFirstGi + k;
|
|
4933
|
+
var g = shownGroups[k];
|
|
4934
|
+
if (!g || g.ghost || !g.headings) continue;
|
|
4935
|
+
var added = 0;
|
|
4936
|
+
for (var hi = 0; hi < g.headings.length; hi++) {
|
|
4937
|
+
var h = g.headings[hi];
|
|
4938
|
+
var lv = Math.min(6, Math.max(1, h.level || 1));
|
|
4939
|
+
if (lv > 3) continue; // H4 and deeper stay out of the ruler
|
|
4940
|
+
all.push({ key: h.key, idx: h.idx, gi: gi, level: lv, title: h.title || "" });
|
|
4941
|
+
added++;
|
|
4942
|
+
}
|
|
4943
|
+
// keep at least one tick per turn that has any H1/H2
|
|
4944
|
+
if (!added && g.headings.length) {
|
|
4945
|
+
var first = g.headings[0];
|
|
4946
|
+
all.push({ key: first.key, idx: first.idx, gi: gi, level: Math.min(6, Math.max(1, first.level || 1)), title: first.title || "" });
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
return all;
|
|
4950
|
+
}, [shownGroups, barFirstGi]);
|
|
4951
|
+
// A very long window can hold thousands of headings. Up to BAR_CAP every heading
|
|
4952
|
+
// gets its own slot (the tape grows instead of dropping any); beyond it the set is
|
|
4953
|
+
// sampled evenly by index, because a DOM node per heading rewritten on every
|
|
4954
|
+
// scroll frame is what makes the lane expensive.
|
|
4955
|
+
var BAR_CAP = 400;
|
|
4956
|
+
var barSlice = barTicks;
|
|
4957
|
+
if (barTicks.length > BAR_CAP) {
|
|
4958
|
+
var thinStep = barTicks.length / BAR_CAP;
|
|
4959
|
+
var keptTicks = [];
|
|
4960
|
+
for (var ck = 0; ck < BAR_CAP; ck++) keptTicks.push(barTicks[Math.min(barTicks.length - 1, Math.round(ck * thinStep))]);
|
|
4961
|
+
barSlice = keptTicks;
|
|
4962
|
+
}
|
|
4963
|
+
// The tape is laid out from the rendered window only (barSlice, capped above): the
|
|
4964
|
+
// layout pass gives every heading a slot at least TICK_MIN_GAP apart, and the tape
|
|
4965
|
+
// slides inside its clipped window when the slots do not all fit. Nothing here
|
|
4966
|
+
// reads row geometry any more, so a scroll never forces a layout read.
|
|
4967
|
+
react.useLayoutEffect(function () {
|
|
4968
|
+
layoutScrollBar(listRef.current);
|
|
4969
|
+
}, [open, sheetOpen, barMode, groups.length, visibleCount, shownGroups.length, activeGroup,
|
|
4970
|
+
sheetColW, marksCol, panelW, panelH, prefs.density, levels, barTicks]);
|
|
4971
|
+
var scrollBarEl = groups.length ? react_jsx_runtime.jsx("div", {
|
|
4972
|
+
ref: barHostRef,
|
|
4973
|
+
className: "dqt-bar",
|
|
4974
|
+
"data-dqt-bar": barMode,
|
|
4975
|
+
style: {
|
|
4976
|
+
// An in-flow column immediately right of the list: when the search or
|
|
4977
|
+
// bookmarks column is pushed in, the list gives up width and the lane
|
|
4978
|
+
// follows by ordinary layout — no percentages, no stray overlap.
|
|
4979
|
+
position: "relative",
|
|
4980
|
+
flex: "0 0 " + barLane + "px",
|
|
4981
|
+
alignSelf: "stretch",
|
|
4982
|
+
width: barLane,
|
|
4983
|
+
// The lane sits right beside the outline and nearly on the panel's own rim:
|
|
4984
|
+
// the reader asked for the mark to hug both sides, in the panel and across
|
|
4985
|
+
// the curtain alike (this is the ONE gap constant for the lane).
|
|
4986
|
+
marginRight: Math.round(4 * barScale),
|
|
4987
|
+
boxSizing: "border-box",
|
|
4988
|
+
zIndex: 3,
|
|
4989
|
+
pointerEvents: "none"
|
|
4990
|
+
},
|
|
4991
|
+
children: [
|
|
4992
|
+
// the ruler: a clipped window over the sliding tape
|
|
4993
|
+
react_jsx_runtime.jsx("div", {
|
|
4994
|
+
key: "tapewrap",
|
|
4995
|
+
style: {
|
|
4996
|
+
position: "absolute",
|
|
4997
|
+
top: BAR_INSET_TOP,
|
|
4998
|
+
bottom: BAR_BOTTOM_GAP,
|
|
4999
|
+
left: barMode === "band" ? 5 : 0,
|
|
5000
|
+
width: barTickW,
|
|
5001
|
+
// the slots are laid out inside this window; a tape taller than it slides
|
|
5002
|
+
// behind this clip, exactly like a scrollbar's track
|
|
5003
|
+
overflow: "hidden"
|
|
5004
|
+
},
|
|
5005
|
+
children: react_jsx_runtime.jsx("div", {
|
|
5006
|
+
ref: barTapeRef,
|
|
5007
|
+
"data-dqt-tape": "on",
|
|
5008
|
+
"data-dqt-ticks": String(barSlice.length),
|
|
5009
|
+
style: { position: "absolute", left: 0, top: 0, width: barTickW, height: "100%" },
|
|
5010
|
+
children: barSlice.map(function (t, ti) {
|
|
5011
|
+
var w = Math.round((t.level === 1 ? 10 : (t.level === 2 ? 6 : 3)) * barScale); // H1 long, H2 medium, H3 a dot
|
|
5012
|
+
return react_jsx_runtime.jsx("div", {
|
|
5013
|
+
"data-dqt-tick": String(t.level),
|
|
5014
|
+
"data-dqt-tick-pos": String(ti),
|
|
5015
|
+
title: t.title,
|
|
5016
|
+
onClick: function () { jump(t.key, t.idx); },
|
|
5017
|
+
style: {
|
|
5018
|
+
position: "absolute",
|
|
5019
|
+
left: 0,
|
|
5020
|
+
top: 0, // placed by layoutScrollBar: one slot per heading
|
|
5021
|
+
height: 10,
|
|
5022
|
+
width: barTickW,
|
|
5023
|
+
display: "flex",
|
|
5024
|
+
alignItems: "center",
|
|
5025
|
+
justifyContent: "flex-start",
|
|
5026
|
+
pointerEvents: "auto",
|
|
5027
|
+
cursor: "pointer"
|
|
5028
|
+
},
|
|
5029
|
+
children: react_jsx_runtime.jsx("span", {
|
|
5030
|
+
style: {
|
|
5031
|
+
display: "block",
|
|
5032
|
+
width: w,
|
|
5033
|
+
height: 2,
|
|
5034
|
+
borderRadius: 1,
|
|
5035
|
+
// neutral until layoutScrollBar knows which rows are in view
|
|
5036
|
+
background: "rgba(128,128,128,0.32)",
|
|
5037
|
+
opacity: 0.8
|
|
5038
|
+
}
|
|
5039
|
+
})
|
|
5040
|
+
}, "tick-" + ti);
|
|
5041
|
+
})
|
|
5042
|
+
})
|
|
5043
|
+
}),
|
|
5044
|
+
// the marker: a rounded bar (wider in the curtain) or a frame around the ruler
|
|
5045
|
+
react_jsx_runtime.jsx("div", {
|
|
5046
|
+
key: "thumb",
|
|
5047
|
+
ref: barThumbRef,
|
|
5048
|
+
"data-dqt-barthumb": barMode,
|
|
5049
|
+
onPointerDown: onBarDown,
|
|
5050
|
+
onPointerMove: onBarMove,
|
|
5051
|
+
onPointerUp: onBarUp,
|
|
5052
|
+
onPointerCancel: onBarUp,
|
|
5053
|
+
onMouseEnter: function () { setBarHot(true); },
|
|
5054
|
+
onMouseLeave: function () { if (!barDragRef.current) setBarHot(false); },
|
|
5055
|
+
style: barMode === "band"
|
|
5056
|
+
? {
|
|
5057
|
+
position: "absolute",
|
|
5058
|
+
left: 0,
|
|
5059
|
+
// a 2px gap to the panel's own rim: the band hugs the right edge, as
|
|
5060
|
+
// close as its 1px outline allows
|
|
5061
|
+
right: 2,
|
|
5062
|
+
top: BAR_INSET_TOP,
|
|
5063
|
+
height: 60,
|
|
5064
|
+
borderRadius: 7,
|
|
5065
|
+
// the same material as the current-turn box on the left, so the two
|
|
5066
|
+
// read as one family
|
|
5067
|
+
background: barHot ? "rgba(79,140,255,0.16)" : "rgba(79,140,255,0.10)",
|
|
5068
|
+
boxShadow: barHot ? "inset 0 0 0 1px rgba(79,140,255,0.65)" : "inset 0 0 0 1px rgba(79,140,255,0.40)",
|
|
5069
|
+
// the frame does not grow on hover: it becomes more solid instead
|
|
5070
|
+
transition: "background-color 0.14s ease, box-shadow 0.14s ease",
|
|
5071
|
+
pointerEvents: "auto",
|
|
5072
|
+
cursor: "pointer",
|
|
5073
|
+
touchAction: "none"
|
|
5074
|
+
}
|
|
5075
|
+
: {
|
|
5076
|
+
position: "absolute",
|
|
5077
|
+
right: 4,
|
|
5078
|
+
top: BAR_INSET_TOP,
|
|
5079
|
+
width: 12,
|
|
5080
|
+
height: 24,
|
|
5081
|
+
display: "flex",
|
|
5082
|
+
alignItems: "stretch",
|
|
5083
|
+
justifyContent: "flex-end",
|
|
5084
|
+
pointerEvents: "auto",
|
|
5085
|
+
cursor: "pointer",
|
|
5086
|
+
touchAction: "none"
|
|
5087
|
+
},
|
|
5088
|
+
children: barMode === "band" ? null : react_jsx_runtime.jsx("span", {
|
|
5089
|
+
"data-dqt-barvis": "bar",
|
|
5090
|
+
style: {
|
|
5091
|
+
display: "block",
|
|
5092
|
+
// wide enough that the rounded caps are actually visible, and the
|
|
5093
|
+
// curtain gets a thicker one; hovering makes it thicker still
|
|
5094
|
+
width: (sheetOpen ? 6 : 4) + (barHot ? 2 : 0),
|
|
5095
|
+
height: "100%",
|
|
5096
|
+
borderRadius: 999,
|
|
5097
|
+
background: C.accent,
|
|
5098
|
+
opacity: 0.95,
|
|
5099
|
+
boxShadow: barHot ? "0 0 0 5px rgba(79,140,255,0.10)" : "none",
|
|
5100
|
+
transition: "width 0.14s ease, box-shadow 0.14s ease"
|
|
5101
|
+
}
|
|
5102
|
+
})
|
|
5103
|
+
})
|
|
5104
|
+
]
|
|
5105
|
+
}) : null;
|
|
5106
|
+
|
|
5107
|
+
|
|
4639
5108
|
// the group currently being read (the turn under the middle of the
|
|
4640
5109
|
// CONVERSATION viewport) stays bright in the outline; others are dimmed.
|
|
4641
5110
|
// Every node key of every loaded group -> its group index: mapping ONLY the
|
|
@@ -5234,7 +5703,7 @@ window.__ModuleLoader__.load({
|
|
|
5234
5703
|
rememberRef.current = !!prefs.remember;
|
|
5235
5704
|
// the auto-loader's switch, mirrored for the same reason: its retry fires from an
|
|
5236
5705
|
// older render's closure, and turning the setting off must stop it immediately
|
|
5237
|
-
autoLoadRef.current =
|
|
5706
|
+
autoLoadRef.current = false; // manual only
|
|
5238
5707
|
// Is any of the outline actually on screen? The follow scan is cheap now, but while
|
|
5239
5708
|
// the panel sits collapsed there is no point measuring at scroll-frame rate.
|
|
5240
5709
|
panelVisibleRef.current = !!(open || sheetOpen);
|
|
@@ -7165,14 +7634,16 @@ window.__ModuleLoader__.load({
|
|
|
7165
7634
|
style: {
|
|
7166
7635
|
position: "absolute",
|
|
7167
7636
|
left: 0,
|
|
7168
|
-
// the
|
|
7169
|
-
//
|
|
7170
|
-
right:
|
|
7637
|
+
// the overlay lives INSIDE the pane now, so its right edge must step over
|
|
7638
|
+
// the lane: the bookmarks stop where the outline's rows stop
|
|
7639
|
+
right: barLane + Math.round(4 * barScale),
|
|
7171
7640
|
top: 0,
|
|
7172
7641
|
bottom: 0,
|
|
7173
7642
|
overflowY: "auto",
|
|
7174
7643
|
overflowAnchor: "none",
|
|
7175
|
-
|
|
7644
|
+
// the gap between those rows and the lane (BAR_TEXT_GAP), never the whole
|
|
7645
|
+
// lane width — the lane is a real flex column
|
|
7646
|
+
padding: sheetOpen ? ("0 " + BAR_TEXT_GAP + "px 8px 8px") : ("0 " + BAR_TEXT_GAP + "px 6px 6px"),
|
|
7176
7647
|
direction: (sheetOpen || dockRight) ? "ltr" : "rtl"
|
|
7177
7648
|
},
|
|
7178
7649
|
children: react_jsx_runtime.jsx("div", { style: { direction: "ltr" }, children: markRowsEl })
|
|
@@ -7207,7 +7678,11 @@ window.__ModuleLoader__.load({
|
|
|
7207
7678
|
// block, so a padded scroll container would hold the pinned group
|
|
7208
7679
|
// header that many pixels below the toolbar (a visible gap). Spacing
|
|
7209
7680
|
// between groups comes from the per-group divider instead.
|
|
7210
|
-
padding
|
|
7681
|
+
// the right padding is the indicator's lane (see barLane above): rows and the
|
|
7682
|
+
// current-group box stop short of it, in the outline and in the marks overlay
|
|
7683
|
+
// the lane is a real flex column now, so the list only needs the air between
|
|
7684
|
+
// its text and that column (BAR_TEXT_GAP), not the whole lane width
|
|
7685
|
+
padding: sheetOpen ? ("0 " + BAR_TEXT_GAP + "px 8px 8px") : ("0 " + BAR_TEXT_GAP + "px 6px 6px"),
|
|
7211
7686
|
// the rows dissolve at BOTH edges instead of being cut by a hard line. The
|
|
7212
7687
|
// top fade is safe now that the day strip lives outside the scroller: it
|
|
7213
7688
|
// thins the rows approaching the strip, never the label itself.
|
|
@@ -7215,15 +7690,9 @@ window.__ModuleLoader__.load({
|
|
|
7215
7690
|
WebkitMaskImage: "linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 22px), transparent 100%)",
|
|
7216
7691
|
flex: "1 1 auto",
|
|
7217
7692
|
minHeight: 0,
|
|
7218
|
-
// across the curtain the
|
|
7219
|
-
//
|
|
7220
|
-
//
|
|
7221
|
-
width: sheetOpen ? "100%" : undefined,
|
|
7222
|
-
maxWidth: sheetOpen ? "90%" : undefined,
|
|
7223
|
-
margin: sheetOpen ? "0 auto" : undefined,
|
|
7224
|
-
// the 20px side padding sits INSIDE the 90% (content-box would make the
|
|
7225
|
-
// rendered column 90% + 40px, i.e. ~93% of the curtain)
|
|
7226
|
-
boxSizing: sheetOpen ? "border-box" : undefined,
|
|
7693
|
+
// across the curtain the WIDTH and centring belong to the pane that holds
|
|
7694
|
+
// this list AND its lane (see bodyRowEl): the lane has to travel with the
|
|
7695
|
+
// outline, or it sits way off at the sheet's edge instead of beside the rows
|
|
7227
7696
|
// the scrollbar follows the dock side in the narrow panel; across the
|
|
7228
7697
|
// curtain it sits on the list's right edge
|
|
7229
7698
|
direction: (sheetOpen || dockRight) ? "ltr" : "rtl"
|
|
@@ -7334,12 +7803,35 @@ window.__ModuleLoader__.load({
|
|
|
7334
7803
|
// would go on showing the last day the outline was scrolled to
|
|
7335
7804
|
children: onlyMarks ? "" : dayStripText
|
|
7336
7805
|
});
|
|
7337
|
-
|
|
7338
|
-
|
|
7806
|
+
// The outline AND its lane travel together. Across the curtain the pair is one
|
|
7807
|
+
// centred column (90% of the sheet at most): centring the LIST alone used to
|
|
7808
|
+
// leave the lane parked at the sheet's edge, nowhere near the rows it describes,
|
|
7809
|
+
// and opening the search column only looked closer because everything shrank.
|
|
7810
|
+
// In the docked panel the pane is simply the full row, so the lane sits right
|
|
7811
|
+
// beside the outline there too.
|
|
7812
|
+
var outlinePaneEl = react_jsx_runtime.jsx("div", {
|
|
7813
|
+
className: "dqt-outline-pane",
|
|
7339
7814
|
// `position: relative` is what lets the bookmark overlay cover the list
|
|
7340
7815
|
// without becoming part of its scrolled content
|
|
7816
|
+
style: {
|
|
7817
|
+
position: "relative",
|
|
7818
|
+
flex: "1 1 auto",
|
|
7819
|
+
minWidth: 0,
|
|
7820
|
+
minHeight: 0,
|
|
7821
|
+
display: "flex",
|
|
7822
|
+
flexDirection: "row",
|
|
7823
|
+
alignItems: "stretch",
|
|
7824
|
+
width: sheetOpen ? "100%" : undefined,
|
|
7825
|
+
maxWidth: sheetOpen ? "90%" : undefined,
|
|
7826
|
+
margin: sheetOpen ? "0 auto" : undefined,
|
|
7827
|
+
boxSizing: sheetOpen ? "border-box" : undefined
|
|
7828
|
+
},
|
|
7829
|
+
children: [listEl, marksOverlayEl, scrollBarEl]
|
|
7830
|
+
});
|
|
7831
|
+
var bodyRowEl = react_jsx_runtime.jsx("div", {
|
|
7832
|
+
className: "dqt-body-row",
|
|
7341
7833
|
style: { position: "relative", flex: "1 1 auto", minHeight: 0, display: "flex", flexDirection: "row", alignItems: "stretch" },
|
|
7342
|
-
children: [
|
|
7834
|
+
children: [outlinePaneEl, sheetSearchColEl]
|
|
7343
7835
|
});
|
|
7344
7836
|
// one geometry for the panel and its shadow twin, merged (never duplicated)
|
|
7345
7837
|
var mergeStyle = function (base, extra) {
|
|
@@ -7777,7 +8269,7 @@ window.__ModuleLoader__.load({
|
|
|
7777
8269
|
// a narrow conversation column leaves no room for a handle floating over the text:
|
|
7778
8270
|
// below the threshold it slides back into the divider (its usual motion), while the
|
|
7779
8271
|
// droplet above the conversation stays put as the way in
|
|
7780
|
-
var edgeShown = !open && handleShown && chatViewActive && !narrowWindow;
|
|
8272
|
+
var edgeShown = !open && panelMode && handleShown && chatViewActive && !narrowWindow;
|
|
7781
8273
|
// The pill is 12px centred in the 26px box, so its edge sits 7px inside the box
|
|
7782
8274
|
// edge. Left dock: the panel opens at `area + 8`, so parking the box 1px out puts
|
|
7783
8275
|
// the pill's left edge exactly on the panel's own left edge — one line.
|
|
@@ -7789,7 +8281,7 @@ window.__ModuleLoader__.load({
|
|
|
7789
8281
|
// than two lines nobody sees at the same time.
|
|
7790
8282
|
var edgeLineLeft = (viewport ? viewport.left : 0) + 1;
|
|
7791
8283
|
var edgeLineRight = (viewport ? viewport.right + 52 : 60); // clear of the milestone rail
|
|
7792
|
-
var edgeEl = (!open || !edgeGone) ? react_jsx_runtime.jsx("div", {
|
|
8284
|
+
var edgeEl = panelMode && (!open || !edgeGone) ? react_jsx_runtime.jsx("div", {
|
|
7793
8285
|
style: {
|
|
7794
8286
|
position: "fixed",
|
|
7795
8287
|
width: EDGE_W,
|
|
@@ -7893,7 +8385,7 @@ window.__ModuleLoader__.load({
|
|
|
7893
8385
|
// 140 wide and only 13.5 tall: the reader asked for a wider, flatter droplet
|
|
7894
8386
|
// (it started at 92, then 120).
|
|
7895
8387
|
var DROP_W = 140;
|
|
7896
|
-
var topHandleEl = (!sheetOpen || !sheetHandleGone) && chatViewActive ? react_jsx_runtime.jsx("div", {
|
|
8388
|
+
var topHandleEl = curtainMode && (!sheetOpen || !sheetHandleGone) && chatViewActive ? react_jsx_runtime.jsx("div", {
|
|
7897
8389
|
className: "dqt-sheet-handle",
|
|
7898
8390
|
"data-sheet-handle": "on",
|
|
7899
8391
|
style: {
|
|
@@ -8450,7 +8942,10 @@ window.__ModuleLoader__.load({
|
|
|
8450
8942
|
// inside the curtain the SHEET is the glass (it has no opacity of its own,
|
|
8451
8943
|
// so its backdrop-filter samples the page): no second twin here.
|
|
8452
8944
|
// The grip is the curtain's own bottom edge: drag it to set the height.
|
|
8453
|
-
|
|
8945
|
+
// the docked panel is not BUILT at all when the reader chose the curtain only:
|
|
8946
|
+
// a parked, clipped box still costs a layer and a shadow twin, and there is no
|
|
8947
|
+
// way to open it once its edge pill is gone
|
|
8948
|
+
children: sheetOpen ? [panelEl, sheetGripEl] : (panelMode ? [panelShadowEl, panelEl] : [])
|
|
8454
8949
|
})]
|
|
8455
8950
|
});
|
|
8456
8951
|
|
|
@@ -9681,6 +10176,16 @@ window.__ModuleLoader__.load({
|
|
|
9681
10176
|
field("dock", T("settings.dock"), "",
|
|
9682
10177
|
seg([["left", T("settings.dock.left")], ["right", T("settings.dock.right")]], prefs.dock,
|
|
9683
10178
|
function (v) { commit({ dock: v }); }), true),
|
|
10179
|
+
// one joined frame, three options: the reader chooses which views EXIST, not
|
|
10180
|
+
// which one happens to be in front
|
|
10181
|
+
field("surface", T("settings.surface"), "",
|
|
10182
|
+
seg([["both", T("settings.surface.both")], ["panel", T("settings.surface.panel")], ["curtain", T("settings.surface.curtain")]],
|
|
10183
|
+
prefs.surface || "both", function (v) { commit({ surface: v }); }), true),
|
|
10184
|
+
// how the scroll position is drawn: a short thumb, or a band that also shows
|
|
10185
|
+
// how much of the list is on screen. The ticks are there either way.
|
|
10186
|
+
field("scrollbar", T("settings.scrollbar"), "",
|
|
10187
|
+
seg([["bar", T("settings.scrollbar.bar")], ["band", T("settings.scrollbar.band")]],
|
|
10188
|
+
prefs.scrollbar || "bar", function (v) { commit({ scrollbar: v }); }), true),
|
|
9684
10189
|
field("levels", T("settings.levels"), "", levelsControl(true), true),
|
|
9685
10190
|
field("density", T("settings.density"), "",
|
|
9686
10191
|
seg([["comfy", T("settings.density.comfy")], ["compact", T("settings.density.compact")]], prefs.density,
|
|
@@ -9689,7 +10194,13 @@ window.__ModuleLoader__.load({
|
|
|
9689
10194
|
accentDots(prefs.accent, function (v) { commit({ accent: v }); }), true),
|
|
9690
10195
|
field("glass", T("settings.glass"),
|
|
9691
10196
|
T("settings.glass.tip") + (autoGlassOff ? " " + T("settings.glass.autoOff") : ""),
|
|
9692
|
-
|
|
10197
|
+
// Turning the material ON is also the reader's answer to an automatic
|
|
10198
|
+
// verdict: the tip already promises "you can turn it back on", so honour it
|
|
10199
|
+
// (the flag lives in localStorage, and the next render reads it again).
|
|
10200
|
+
onoff(prefs.glass, function (v) {
|
|
10201
|
+
if (v) clearAutoGlassOff();
|
|
10202
|
+
commit({ glass: v });
|
|
10203
|
+
}), true),
|
|
9693
10204
|
field("glassStrength", T("settings.glassStrength"), T("settings.glassStrength.tip"), glassStrengthControl, false),
|
|
9694
10205
|
field("zoom", T("settings.zoom"), T("settings.zoom.tip"), zoomControl, false),
|
|
9695
10206
|
field("sheetZoom", T("settings.sheetZoom"), T("settings.sheetZoom.tip"), sheetZoomControl, false),
|
|
@@ -9697,7 +10208,6 @@ window.__ModuleLoader__.load({
|
|
|
9697
10208
|
field("fuzzy", T("settings.fuzzy"), "", onoff(prefs.fuzzy, function (v) { commit({ fuzzy: v }); }), true),
|
|
9698
10209
|
field("hover", T("settings.hover"), "", onoff(prefs.hover, function (v) { commit({ hover: v }); }), true),
|
|
9699
10210
|
field("remember", T("settings.remember"), "", onoff(prefs.remember, function (v) { commit({ remember: v }); }), true),
|
|
9700
|
-
field("autoLoad", T("settings.autoLoad"), "", onoff(prefs.autoLoad, function (v) { commit({ autoLoad: v }); }), true),
|
|
9701
10211
|
field("debug", T("settings.debug"), T("settings.debug.tip"), onoff(prefs.debug, function (v) { commit({ debug: v }); }), true, true)
|
|
9702
10212
|
];
|
|
9703
10213
|
return react_jsx_runtime.jsxs("li", {
|
package/lib/index.js
CHANGED
|
@@ -41,6 +41,11 @@ export const Config = z.object({
|
|
|
41
41
|
.pattern(/^(left|right)$/)
|
|
42
42
|
.default("left")
|
|
43
43
|
.description("Which edge the panel docks to by default")),
|
|
44
|
+
surface: volatileSchema(z
|
|
45
|
+
.string()
|
|
46
|
+
.pattern(/^(both|panel|curtain)$/)
|
|
47
|
+
.default("both")
|
|
48
|
+
.description("Which views the plugin offers: both keeps the docked panel and the curtain, panel removes the curtain, curtain removes the docked panel")),
|
|
44
49
|
levels: volatileSchema(z
|
|
45
50
|
.array(z.number().min(1).max(6))
|
|
46
51
|
.default([1, 2, 3, 4, 5, 6])
|
|
@@ -71,6 +76,11 @@ export const Config = z.object({
|
|
|
71
76
|
.max(1)
|
|
72
77
|
.default(1)
|
|
73
78
|
.description("Height of the curtain as a fraction of the conversation area; drag its bottom edge to change it")),
|
|
79
|
+
scrollbar: volatileSchema(z
|
|
80
|
+
.string()
|
|
81
|
+
.pattern(/^(bar|band)$/)
|
|
82
|
+
.default("bar")
|
|
83
|
+
.description("How the outline draws its scroll position on the right edge: bar is a short vertical thumb, band marks how much of the list is visible")),
|
|
74
84
|
zoom: volatileSchema(z
|
|
75
85
|
.number()
|
|
76
86
|
.min(0.5)
|
|
@@ -101,10 +111,7 @@ export const Config = z.object({
|
|
|
101
111
|
.boolean()
|
|
102
112
|
.default(true)
|
|
103
113
|
.description("Remember where you were reading in each session and return there when it is reopened")),
|
|
104
|
-
|
|
105
|
-
.boolean()
|
|
106
|
-
.default(true)
|
|
107
|
-
.description("Load history automatically when the outline is scrolled into turns that are not loaded yet; off means nothing loads until a row is clicked")),
|
|
114
|
+
|
|
108
115
|
debug: volatileSchema(z
|
|
109
116
|
.boolean()
|
|
110
117
|
.default(false)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quick-toc",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "Conversation TOC for DeepSeek Harness (DSH): a whole-session, turn-grouped Markdown outline with hover previews, heading-level preferences, a full-width curtain, jump-to-end buttons, title/prompts/full-text/cross-session search and a Chinese/English interface.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"LICENSE"
|
|
18
18
|
],
|
|
19
19
|
"engines": {
|
|
20
|
-
"dsh": ">=0.1
|
|
20
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0-0"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test": "node test/render.mjs"
|
|
@@ -42,28 +42,19 @@
|
|
|
42
42
|
},
|
|
43
43
|
"compatibility": {
|
|
44
44
|
"dshReleases": {
|
|
45
|
-
"0.1
|
|
46
|
-
"0.1
|
|
47
|
-
"0.1
|
|
48
|
-
"0.1
|
|
49
|
-
"0.
|
|
50
|
-
"0.2.0-rc.1": "compatible",
|
|
51
|
-
"0.2.0-rc.2": "compatible",
|
|
52
|
-
"0.2.1-alpha.1": "compatible",
|
|
53
|
-
"0.2.1-alpha.2": "compatible",
|
|
54
|
-
"0.2.1-alpha.3": "compatible",
|
|
55
|
-
"0.2.1-alpha.4": "compatible",
|
|
56
|
-
"0.2.1-alpha.5": "compatible",
|
|
57
|
-
"0.2.1-rc.1": "compatible",
|
|
58
|
-
"0.2.1-rc.2": "compatible"
|
|
45
|
+
"0.2.1-rc.2": "compatible",
|
|
46
|
+
"0.2.1-rc.3": "compatible",
|
|
47
|
+
"0.2.1-rc.4": "compatible",
|
|
48
|
+
"0.2.1-rc.5": "compatible",
|
|
49
|
+
"0.2.2-alpha.2": "compatible"
|
|
59
50
|
}
|
|
60
51
|
}
|
|
61
52
|
},
|
|
62
53
|
"peerDependencies": {
|
|
63
54
|
"react": "^18.2.0",
|
|
64
55
|
"@deepseek-ai/schemastery": "^3.18.0",
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-chat": ">=0.1
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-chat": ">=0.2.1-rc.2 <0.3.0-0",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.2.1-rc.2 <0.3.0-0"
|
|
67
58
|
},
|
|
68
59
|
"peerDependenciesMeta": {
|
|
69
60
|
"@deepseek-ai/schemastery": {
|