dsh-quick-toc 0.4.0 → 0.5.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 +32 -8
- package/CHANGELOG.md +32 -8
- package/README.en.md +32 -26
- package/README.md +32 -26
- package/lib/client.js +1356 -361
- package/package.json +5 -3
package/lib/client.js
CHANGED
|
@@ -21,6 +21,57 @@ window.__ModuleLoader__.load({
|
|
|
21
21
|
var Z_BASE = 500;
|
|
22
22
|
var EASE = "cubic-bezier(0.22, 0.9, 0.3, 1)"; // smooth non-linear slide
|
|
23
23
|
|
|
24
|
+
// ---------- UI strings ----------
|
|
25
|
+
// Every string the panel renders goes through T(). 0.5.0 ships the Chinese
|
|
26
|
+
// column only — the visible text is byte-identical to 0.4.1 — so 0.6.0 can
|
|
27
|
+
// add English (or feed the same keys into the host locale table) without
|
|
28
|
+
// hunting for hard-coded literals a second time.
|
|
29
|
+
var DICTS = {
|
|
30
|
+
zh: {
|
|
31
|
+
"panel.title": "对话大纲",
|
|
32
|
+
"error.panel": "dsh-quick-toc 面板出错: ",
|
|
33
|
+
"handle.dragY": "按住拖动调整位置",
|
|
34
|
+
"handle.dockLeft": "移到左侧",
|
|
35
|
+
"handle.dockRight": "移到右侧",
|
|
36
|
+
"handle.collapse": "收起",
|
|
37
|
+
"handle.expand": "展开大纲",
|
|
38
|
+
"levels.tip": "标题层级筛选",
|
|
39
|
+
"levels.show": "显示 H",
|
|
40
|
+
"levels.hide": "隐藏 H",
|
|
41
|
+
"search.open": "搜索标题",
|
|
42
|
+
"search.word": "搜索",
|
|
43
|
+
"search.tail": ",回车定位…",
|
|
44
|
+
"search.scope.title": "标题",
|
|
45
|
+
"search.scope.full": "全文",
|
|
46
|
+
"search.scope.tipTitle": "当前:仅搜索标题。点击切换为全文搜索",
|
|
47
|
+
"search.scope.tipFull": "当前:全文搜索。点击切换为仅标题",
|
|
48
|
+
"search.fuzzy": "模糊",
|
|
49
|
+
"search.fuzzy.tipOn": "模糊匹配已开启:允许关键字中间夹少量其他文字,命中更多",
|
|
50
|
+
"search.fuzzy.tipOff": "模糊匹配已关闭:只匹配连续的文字。点击开启",
|
|
51
|
+
"search.empty": "没有匹配",
|
|
52
|
+
"search.hintMore": "向上滚动可加载更早的消息",
|
|
53
|
+
"search.hintOldest": "已经是最早的消息",
|
|
54
|
+
"hint.bottom": "已经到底了",
|
|
55
|
+
"turn.jumpReply": "跳转到该回合的模型回答开头",
|
|
56
|
+
"turn.jumpTurn": "跳转到该回合开头",
|
|
57
|
+
"turn.loading": "正在加载这个回合…",
|
|
58
|
+
"turn.unloaded": "未加载",
|
|
59
|
+
"turn.loadTip": "点击加载这个回合并跳转过去",
|
|
60
|
+
"preview.truncated": "预览由宿主提供,可能被截断",
|
|
61
|
+
"resize.w": "拖拽调整宽度",
|
|
62
|
+
"resize.h": "拖拽调整高度",
|
|
63
|
+
"resize.wh": "拖拽同时调整宽高"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// The dictionary the current render reads. Kept as a module-level binding so
|
|
67
|
+
// the module-scope render helpers (renderItem/renderResultRow/...) can localise
|
|
68
|
+
// without threading a `t` argument through every call.
|
|
69
|
+
var activeDict = DICTS.zh;
|
|
70
|
+
function T(key) {
|
|
71
|
+
var s = activeDict[key];
|
|
72
|
+
return s === undefined ? key : s;
|
|
73
|
+
}
|
|
74
|
+
|
|
24
75
|
// ---------- markdown helpers ----------
|
|
25
76
|
function extractReplyText(node) {
|
|
26
77
|
var blocks = node && node.data && node.data.blocks;
|
|
@@ -78,6 +129,153 @@ window.__ModuleLoader__.load({
|
|
|
78
129
|
return first;
|
|
79
130
|
}
|
|
80
131
|
|
|
132
|
+
// ---------- normalized matching (fold, plus an optional fuzzy pass) ----------
|
|
133
|
+
// Folding ignores differences that never change meaning: letter case,
|
|
134
|
+
// full-width vs half-width forms (AB12,), and runs of whitespace. `map`
|
|
135
|
+
// keeps every folded character's ORIGINAL index, so a match found in folded
|
|
136
|
+
// space still highlights the untouched DOM text. Folding is ALWAYS on — it is
|
|
137
|
+
// correctness, not fuzziness: pasting "全角" text must find "全角" content.
|
|
138
|
+
function foldWithMap(text) {
|
|
139
|
+
var src = String(text === undefined || text === null ? "" : text);
|
|
140
|
+
var out = "";
|
|
141
|
+
var map = [];
|
|
142
|
+
var prevSpace = false;
|
|
143
|
+
for (var i = 0; i < src.length; i++) {
|
|
144
|
+
var ch = src.charAt(i);
|
|
145
|
+
var code = src.charCodeAt(i);
|
|
146
|
+
if (code >= 0xFF01 && code <= 0xFF5E) ch = String.fromCharCode(code - 0xFEE0); // full-width ASCII
|
|
147
|
+
else if (code === 0x3000) ch = " "; // ideographic space
|
|
148
|
+
var lower = ch.toLowerCase();
|
|
149
|
+
if (/\s/.test(lower)) {
|
|
150
|
+
if (prevSpace) continue; // collapse a whitespace run to one space
|
|
151
|
+
prevSpace = true;
|
|
152
|
+
out += " ";
|
|
153
|
+
map.push(i);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
prevSpace = false;
|
|
157
|
+
out += lower;
|
|
158
|
+
map.push(i);
|
|
159
|
+
}
|
|
160
|
+
map.push(src.length); // end sentinel: source index just past the last character
|
|
161
|
+
return { text: out, map: map };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function foldQuery(q) {
|
|
165
|
+
return foldWithMap(q).text.trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Contiguous occurrences of the folded query — the panel's original
|
|
169
|
+
// behaviour (case-insensitive, non-overlapping), now fold-aware.
|
|
170
|
+
function exactMatches(folded, fq) {
|
|
171
|
+
var hits = [];
|
|
172
|
+
if (!fq) return hits;
|
|
173
|
+
var at = 0;
|
|
174
|
+
while ((at = folded.text.indexOf(fq, at)) !== -1) {
|
|
175
|
+
hits.push({ ranges: [[folded.map[at], folded.map[at + fq.length]]] });
|
|
176
|
+
at += fq.length;
|
|
177
|
+
}
|
|
178
|
+
return hits;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// How far apart the matched characters of one fuzzy hit may sit. Without a
|
|
182
|
+
// bound, "提交" would match any text that happens to contain 提…交 somewhere,
|
|
183
|
+
// which buries the real hits; the span keeps fuzzy useful for "关键字中间夹
|
|
184
|
+
// 了别的字" without turning the list into noise.
|
|
185
|
+
function fuzzySpan(fq) {
|
|
186
|
+
return Math.max(12, fq.length * 2 + 8);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Subsequence occurrences (fuzzy mode): the query's characters must appear in
|
|
190
|
+
// order, each hit is the shortest window found from the current position, and
|
|
191
|
+
// hits never overlap. Adjacent matched characters are merged into one range so
|
|
192
|
+
// a contiguous hit still highlights as a whole word.
|
|
193
|
+
function fuzzyMatches(folded, fq) {
|
|
194
|
+
var hits = [];
|
|
195
|
+
if (!fq) return hits;
|
|
196
|
+
var span = fuzzySpan(fq);
|
|
197
|
+
var from = 0;
|
|
198
|
+
for (;;) {
|
|
199
|
+
var start = -1;
|
|
200
|
+
var at = from;
|
|
201
|
+
var ranges = [];
|
|
202
|
+
var ok = true;
|
|
203
|
+
for (var i = 0; i < fq.length; i++) {
|
|
204
|
+
var hit = folded.text.indexOf(fq.charAt(i), at);
|
|
205
|
+
if (hit === -1 || (start !== -1 && hit - start > span)) { ok = false; break; }
|
|
206
|
+
if (start === -1) start = hit;
|
|
207
|
+
var raw = [folded.map[hit], folded.map[hit + 1]];
|
|
208
|
+
var last = ranges.length > 0 ? ranges[ranges.length - 1] : null;
|
|
209
|
+
if (last !== null && last[1] === raw[0] && folded.text.charAt(hit - 1) !== " ") last[1] = raw[1];
|
|
210
|
+
else ranges.push(raw);
|
|
211
|
+
at = hit + 1;
|
|
212
|
+
}
|
|
213
|
+
if (!ok) break;
|
|
214
|
+
hits.push({ ranges: ranges });
|
|
215
|
+
from = at;
|
|
216
|
+
if (from >= folded.text.length) break;
|
|
217
|
+
}
|
|
218
|
+
return hits;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function findMatches(folded, fq, fuzzy) {
|
|
222
|
+
if (!fq) return [];
|
|
223
|
+
return fuzzy ? fuzzyMatches(folded, fq) : exactMatches(folded, fq);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Number of hits of the folded query in `text` (used for ×N and n/N counts).
|
|
227
|
+
function countOccurrences(text, q, fuzzy) {
|
|
228
|
+
var fq = q && q.folded !== undefined ? q.folded : foldQuery(q);
|
|
229
|
+
if (!text || !fq) return 0;
|
|
230
|
+
return findMatches(foldWithMap(text), fq, !!fuzzy).length;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------- section bodies: subtitle + hover preview ----------
|
|
234
|
+
// Body lines of one heading: everything after it up to the next heading in
|
|
235
|
+
// the same message.
|
|
236
|
+
function sectionPreview(lines, from, to, max) {
|
|
237
|
+
if (!lines || from >= to) return "";
|
|
238
|
+
var out = "";
|
|
239
|
+
var fence = null;
|
|
240
|
+
for (var i = from; i < to; i++) {
|
|
241
|
+
var line = lines[i];
|
|
242
|
+
var fm = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
243
|
+
if (fence !== null) {
|
|
244
|
+
if (fm && fm[1].charAt(0) === fence.char && fm[1].length >= fence.len && fm[2].trim() === "") fence = null;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (fm) { fence = { char: fm[1].charAt(0), len: fm[1].length }; continue; }
|
|
248
|
+
var t = line.trim();
|
|
249
|
+
if (t === "") continue;
|
|
250
|
+
out += (out === "" ? "" : " ") + t;
|
|
251
|
+
if (out.length >= max) return out.slice(0, max).trim() + "…";
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// First readable sentence of a section — the outline row's subtitle. Skips
|
|
257
|
+
// blank lines, code fences, table rules and bare bullet/emphasis markers so a
|
|
258
|
+
// "###" block that opens with a table still gets a meaningful line.
|
|
259
|
+
function firstSentence(lines, from, to, max) {
|
|
260
|
+
if (!lines || from >= to) return "";
|
|
261
|
+
var fence = null;
|
|
262
|
+
for (var i = from; i < to; i++) {
|
|
263
|
+
var line = lines[i];
|
|
264
|
+
var fm = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
265
|
+
if (fence !== null) {
|
|
266
|
+
if (fm && fm[1].charAt(0) === fence.char && fm[1].length >= fence.len && fm[2].trim() === "") fence = null;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (fm) { fence = { char: fm[1].charAt(0), len: fm[1].length }; continue; }
|
|
270
|
+
var t = line.trim().replace(/^[>+\-*]\s+/, "").replace(/^\d+[.)]\s+/, "");
|
|
271
|
+
if (t === "" || /^[|\-=*_\s]+$/.test(t)) continue;
|
|
272
|
+
t = cleanTitle(t);
|
|
273
|
+
if (t === "") continue;
|
|
274
|
+
return t.length > max ? t.slice(0, max).trim() + "…" : t;
|
|
275
|
+
}
|
|
276
|
+
return "";
|
|
277
|
+
}
|
|
278
|
+
|
|
81
279
|
// Headings are collected line by line so FENCED CODE BLOCKS can be skipped:
|
|
82
280
|
// a "```" block that documents markdown (or shows a shell comment like
|
|
83
281
|
// "# install") contains lines that look like headings but are code — they
|
|
@@ -99,18 +297,18 @@ window.__ModuleLoader__.load({
|
|
|
99
297
|
}
|
|
100
298
|
if (fm) { fence = { char: fm[1].charAt(0), len: fm[1].length }; continue; }
|
|
101
299
|
var m = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
102
|
-
if (m !== null) items.push({ level: m[1].length, title: cleanTitle(m[2].trim()) });
|
|
300
|
+
if (m !== null) items.push({ level: m[1].length, title: cleanTitle(m[2].trim()), line: i });
|
|
103
301
|
}
|
|
104
302
|
return items;
|
|
105
303
|
}
|
|
106
304
|
|
|
107
|
-
function buildTree(headings) {
|
|
305
|
+
function buildTree(headings, time) {
|
|
108
306
|
var root = { level: 0, children: [] };
|
|
109
307
|
var stack = [root];
|
|
110
308
|
for (var i = 0; i < headings.length; i++) {
|
|
111
309
|
var h = headings[i];
|
|
112
310
|
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) stack.pop();
|
|
113
|
-
var node = { level: h.level, title: h.title, key: h.key, idx: h.idx, children: [] };
|
|
311
|
+
var node = { level: h.level, title: h.title, key: h.key, idx: h.idx, sub: h.sub, preview: h.preview, time: time, children: [] };
|
|
114
312
|
stack[stack.length - 1].children.push(node);
|
|
115
313
|
stack.push(node);
|
|
116
314
|
}
|
|
@@ -122,6 +320,7 @@ window.__ModuleLoader__.load({
|
|
|
122
320
|
// so a streaming update only re-joins/re-parses the node that actually
|
|
123
321
|
// changed instead of every message in the history.
|
|
124
322
|
var EMPTY_HEADINGS = [];
|
|
323
|
+
var EMPTY_LINES = [];
|
|
125
324
|
var nodeInfoCache = new Map();
|
|
126
325
|
function nodeInfo(key, node) {
|
|
127
326
|
var isUser = node.kind === "user";
|
|
@@ -132,12 +331,24 @@ window.__ModuleLoader__.load({
|
|
|
132
331
|
return cached;
|
|
133
332
|
}
|
|
134
333
|
var text = isUser ? extractUserText(node) : extractReplyText(node);
|
|
334
|
+
var lines = text ? text.split("\n") : EMPTY_LINES;
|
|
335
|
+
var headings = node.kind === "assistant-step" ? parseHeadings(text) : EMPTY_HEADINGS;
|
|
336
|
+
// per-heading subtitle (first sentence) + hover preview (section opening) —
|
|
337
|
+
// computed here, with the parse, so streaming updates only pay for the node
|
|
338
|
+
// that actually changed. `line` marks where the heading sits in `lines`, and
|
|
339
|
+
// its section runs to the next heading of ANY level (or the message end).
|
|
340
|
+
for (var hi = 0; hi < headings.length; hi++) {
|
|
341
|
+
var toLine = hi + 1 < headings.length ? headings[hi + 1].line : lines.length;
|
|
342
|
+
headings[hi].sub = firstSentence(lines, headings[hi].line + 1, toLine, 44);
|
|
343
|
+
headings[hi].preview = sectionPreview(lines, headings[hi].line + 1, toLine, 260);
|
|
344
|
+
}
|
|
135
345
|
var info = {
|
|
136
346
|
node: node,
|
|
137
347
|
blocks: blocks,
|
|
138
348
|
user: isUser,
|
|
139
349
|
text: text,
|
|
140
|
-
|
|
350
|
+
lines: lines,
|
|
351
|
+
headings: headings
|
|
141
352
|
};
|
|
142
353
|
nodeInfoCache.set(key, info);
|
|
143
354
|
return info;
|
|
@@ -155,33 +366,44 @@ window.__ModuleLoader__.load({
|
|
|
155
366
|
return h.key + "#" + h.idx;
|
|
156
367
|
}
|
|
157
368
|
|
|
158
|
-
// Split `text` into runs around
|
|
159
|
-
//
|
|
160
|
-
|
|
369
|
+
// Split `text` into runs around the hits of `q`, so the caller can render the
|
|
370
|
+
// hits distinctly. Returns [{ text, hit }, ...]. Runs on folded text, so a
|
|
371
|
+
// full-width or differently-cased match is highlighted where it really sits,
|
|
372
|
+
// and a fuzzy hit paints each matched character run.
|
|
373
|
+
function highlightParts(text, q, fuzzy) {
|
|
161
374
|
var parts = [];
|
|
162
375
|
if (!text) return parts;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
var
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
376
|
+
var fq = foldQuery(q);
|
|
377
|
+
if (!fq) return [{ text: text, hit: false }];
|
|
378
|
+
var hits = findMatches(foldWithMap(text), fq, !!fuzzy);
|
|
379
|
+
if (hits.length === 0) return [{ text: text, hit: false }];
|
|
380
|
+
var cursor = 0;
|
|
381
|
+
for (var i = 0; i < hits.length; i++) {
|
|
382
|
+
var ranges = hits[i].ranges;
|
|
383
|
+
for (var r = 0; r < ranges.length; r++) {
|
|
384
|
+
var from = ranges[r][0];
|
|
385
|
+
var to = ranges[r][1];
|
|
386
|
+
if (from < cursor || to <= from) continue;
|
|
387
|
+
if (from > cursor) parts.push({ text: text.slice(cursor, from), hit: false });
|
|
388
|
+
parts.push({ text: text.slice(from, to), hit: true });
|
|
389
|
+
cursor = to;
|
|
390
|
+
}
|
|
172
391
|
}
|
|
173
|
-
if (
|
|
392
|
+
if (cursor < text.length) parts.push({ text: text.slice(cursor), hit: false });
|
|
174
393
|
return parts;
|
|
175
394
|
}
|
|
176
395
|
|
|
177
396
|
// A one-line window around the first hit of `q` in `text` (for search results).
|
|
178
|
-
function snippetAround(text, q, span) {
|
|
397
|
+
function snippetAround(text, q, span, fuzzy) {
|
|
179
398
|
if (!text) return "";
|
|
180
399
|
var flat = text.replace(/\s+/g, " ").trim();
|
|
181
|
-
var
|
|
182
|
-
|
|
400
|
+
var fq = foldQuery(q);
|
|
401
|
+
var folded = fq ? foldWithMap(flat) : null;
|
|
402
|
+
var hits = folded ? findMatches(folded, fq, !!fuzzy) : [];
|
|
403
|
+
if (hits.length === 0) return previewText(flat, span * 2);
|
|
404
|
+
var at = folded.map[hits[0].ranges[0][0]];
|
|
183
405
|
var start = Math.max(0, at - span);
|
|
184
|
-
var end = Math.min(flat.length, at +
|
|
406
|
+
var end = Math.min(flat.length, at + span * 2);
|
|
185
407
|
return (start > 0 ? "…" : "") + flat.slice(start, end) + (end < flat.length ? "…" : "");
|
|
186
408
|
}
|
|
187
409
|
|
|
@@ -228,21 +450,6 @@ window.__ModuleLoader__.load({
|
|
|
228
450
|
}
|
|
229
451
|
|
|
230
452
|
// count every occurrence of q (case-insensitive) inside text
|
|
231
|
-
function countOccurrences(text, q) {
|
|
232
|
-
// an empty needle would make indexOf return 0 forever and freeze the tab;
|
|
233
|
-
// every caller passes a trimmed non-empty query today, but the guard keeps
|
|
234
|
-
// a future call site from hanging the UI.
|
|
235
|
-
if (!text || !q) return 0;
|
|
236
|
-
var lower = text.toLowerCase();
|
|
237
|
-
var count = 0;
|
|
238
|
-
var idx = 0;
|
|
239
|
-
while ((idx = lower.indexOf(q, idx)) !== -1) {
|
|
240
|
-
count++;
|
|
241
|
-
idx += q.length;
|
|
242
|
-
}
|
|
243
|
-
return count;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
453
|
// find the conversation's "load older messages" button (scoped to the
|
|
247
454
|
// conversation scrollport so panel buttons are never matched)
|
|
248
455
|
function findLoadOlderButton() {
|
|
@@ -320,10 +527,17 @@ window.__ModuleLoader__.load({
|
|
|
320
527
|
highlightSpans = [];
|
|
321
528
|
}
|
|
322
529
|
|
|
323
|
-
// wrap every
|
|
324
|
-
//
|
|
325
|
-
|
|
530
|
+
// wrap every hit of q inside row's text nodes; the hit at `currentOcc` gets a
|
|
531
|
+
// distinct "current" highlight. Matching runs on FOLDED text (case/full-width/
|
|
532
|
+
// whitespace insensitive) and, in fuzzy mode, on subsequences — a hit is then a
|
|
533
|
+
// set of character ranges, so one fuzzy hit can paint several spans.
|
|
534
|
+
// NOTE: these fills sit ON TOP OF conversation text, so they stay deliberately
|
|
535
|
+
// stronger than the panel's UI tints (C.chip / C.groupTint); only the current
|
|
536
|
+
// hit's outline reuses the shared accent value.
|
|
537
|
+
function highlightRow(row, q, currentOcc, fuzzy) {
|
|
326
538
|
if (!row || !q) return;
|
|
539
|
+
var fq = foldQuery(q);
|
|
540
|
+
if (!fq) return;
|
|
327
541
|
var walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT, null);
|
|
328
542
|
var textNodes = [];
|
|
329
543
|
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
|
@@ -332,38 +546,41 @@ window.__ModuleLoader__.load({
|
|
|
332
546
|
var node = textNodes[i];
|
|
333
547
|
var text = node.nodeValue;
|
|
334
548
|
if (!text) continue;
|
|
335
|
-
var
|
|
336
|
-
if (
|
|
549
|
+
var hits = findMatches(foldWithMap(text), fq, !!fuzzy);
|
|
550
|
+
if (hits.length === 0) continue;
|
|
337
551
|
var frag = document.createDocumentFragment();
|
|
338
|
-
// EVERY
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
552
|
+
// EVERY hit inside this text node must be wrapped, in order: the caller
|
|
553
|
+
// addresses hits by their global index (`occ`), which is counted over the
|
|
554
|
+
// message text. Wrapping only the first one per text node made a second
|
|
555
|
+
// hit on the same line un-markable, so stepping to it found no
|
|
342
556
|
// `.dqt-current` and fell back to a plain scroll with no highlight.
|
|
343
|
-
var
|
|
344
|
-
var
|
|
345
|
-
|
|
346
|
-
var before = text.slice(from, idx);
|
|
347
|
-
if (before) frag.appendChild(document.createTextNode(before));
|
|
348
|
-
var mark = document.createElement("span");
|
|
349
|
-
if (occ === currentOcc) {
|
|
350
|
-
mark.className = "dqt-current";
|
|
351
|
-
mark.style.background = "rgba(79,140,255,0.55)";
|
|
352
|
-
mark.style.boxShadow = "0 0 0 1px rgba(79,140,255,0.85)";
|
|
353
|
-
} else {
|
|
354
|
-
mark.style.background = "rgba(79,140,255,0.32)";
|
|
355
|
-
}
|
|
356
|
-
mark.style.borderRadius = "2px";
|
|
357
|
-
mark.style.color = "inherit";
|
|
358
|
-
mark.textContent = text.slice(idx, idx + q.length);
|
|
359
|
-
frag.appendChild(mark);
|
|
360
|
-
highlightSpans.push(mark);
|
|
557
|
+
var cursor = 0;
|
|
558
|
+
for (var h = 0; h < hits.length; h++) {
|
|
559
|
+
var isCurrent = occ === currentOcc;
|
|
361
560
|
occ++;
|
|
362
|
-
|
|
363
|
-
|
|
561
|
+
var ranges = hits[h].ranges;
|
|
562
|
+
for (var r = 0; r < ranges.length; r++) {
|
|
563
|
+
var from = ranges[r][0];
|
|
564
|
+
var to = ranges[r][1];
|
|
565
|
+
if (from < cursor || to <= from) continue;
|
|
566
|
+
if (from > cursor) frag.appendChild(document.createTextNode(text.slice(cursor, from)));
|
|
567
|
+
var mark = document.createElement("span");
|
|
568
|
+
if (isCurrent) {
|
|
569
|
+
mark.className = "dqt-current";
|
|
570
|
+
mark.style.background = "rgba(79,140,255,0.55)";
|
|
571
|
+
mark.style.boxShadow = "0 0 0 1px rgba(79,140,255,0.85)";
|
|
572
|
+
} else {
|
|
573
|
+
mark.style.background = "rgba(79,140,255,0.32)";
|
|
574
|
+
}
|
|
575
|
+
mark.style.borderRadius = "2px";
|
|
576
|
+
mark.style.color = "inherit";
|
|
577
|
+
mark.textContent = text.slice(from, to);
|
|
578
|
+
frag.appendChild(mark);
|
|
579
|
+
highlightSpans.push(mark);
|
|
580
|
+
cursor = to;
|
|
581
|
+
}
|
|
364
582
|
}
|
|
365
|
-
|
|
366
|
-
if (after) frag.appendChild(document.createTextNode(after));
|
|
583
|
+
if (cursor < text.length) frag.appendChild(document.createTextNode(text.slice(cursor)));
|
|
367
584
|
node.parentNode.replaceChild(frag, node);
|
|
368
585
|
}
|
|
369
586
|
}
|
|
@@ -376,7 +593,14 @@ window.__ModuleLoader__.load({
|
|
|
376
593
|
muted: "var(--dsw-alias-label-secondary, #9aa0ab)",
|
|
377
594
|
accent: "var(--dsw-alias-brand-primary, #4f8cff)",
|
|
378
595
|
hover: "var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08))",
|
|
379
|
-
|
|
596
|
+
// ONE source for every "this control is on / this button is open / this match
|
|
597
|
+
// is current" tint, so the panel cannot drift into five near-identical blues.
|
|
598
|
+
chip: "rgba(79, 140, 255, 0.18)",
|
|
599
|
+
// the followed group is a LARGE surface: same hue, lighter fill, with a
|
|
600
|
+
// closing outline + accent bar so the block reads as a finished shape.
|
|
601
|
+
groupTint: "rgba(79, 140, 255, 0.10)",
|
|
602
|
+
groupEdge: "rgba(79, 140, 255, 0.85)",
|
|
603
|
+
groupEdgeSoft: "rgba(79, 140, 255, 0.35)"
|
|
380
604
|
};
|
|
381
605
|
|
|
382
606
|
// ---- theme scheme detection (for the panel's 3D inner shadow) ----
|
|
@@ -444,7 +668,7 @@ window.__ModuleLoader__.load({
|
|
|
444
668
|
maxWidth: 340,
|
|
445
669
|
wordBreak: "break-all"
|
|
446
670
|
},
|
|
447
|
-
children: "
|
|
671
|
+
children: T("error.panel") + msg
|
|
448
672
|
});
|
|
449
673
|
}
|
|
450
674
|
return this.props.children;
|
|
@@ -466,6 +690,68 @@ window.__ModuleLoader__.load({
|
|
|
466
690
|
var order = useChat(function (s) { return s.order; });
|
|
467
691
|
var nodes = useChat(function (s) { return s.nodes; });
|
|
468
692
|
|
|
693
|
+
// Whole-log turn index: the host's `turnOutline` projection
|
|
694
|
+
// (@deepseek-ai/dsh-session-turn-outline) names EVERY turn of the session —
|
|
695
|
+
// including turns the paged event window has not loaded yet — with a bounded
|
|
696
|
+
// prompt/response preview and the `turn/start` seq that pages history to it.
|
|
697
|
+
// On a host without that projection the value is undefined and the panel
|
|
698
|
+
// behaves exactly like 0.4.1 (loaded turns only).
|
|
699
|
+
var useProjection = props.useProjection;
|
|
700
|
+
var rawOutline = useProjection ? useProjection("turnOutline") : null;
|
|
701
|
+
// { sessions() } bridge to the host session face, injected by apply().
|
|
702
|
+
// It is a LOOKUP (ctx.get), not a cached reference: the service may be
|
|
703
|
+
// registered after this package loads, and an unloaded provider must not
|
|
704
|
+
// leave a stale handle behind.
|
|
705
|
+
var tocHost = props.tocHost;
|
|
706
|
+
var sessionId = props.sessionId;
|
|
707
|
+
var hostSessions = function () {
|
|
708
|
+
if (!tocHost || typeof tocHost.sessions !== "function") return null;
|
|
709
|
+
try {
|
|
710
|
+
return tocHost.sessions();
|
|
711
|
+
} catch (e) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
// Which stage of the bridge is actually ready — reported once at mount so a
|
|
716
|
+
// missing jump loader can be diagnosed from the console instead of guessed.
|
|
717
|
+
var jumpLoaderState = function () {
|
|
718
|
+
var sessions = hostSessions();
|
|
719
|
+
if (!sessions) return "no-sessions-service";
|
|
720
|
+
if (typeof sessions.binding !== "function") return "no-binding-api";
|
|
721
|
+
var binding = null;
|
|
722
|
+
try {
|
|
723
|
+
binding = sessions.binding(sessionId);
|
|
724
|
+
} catch (e) {
|
|
725
|
+
return "binding-threw";
|
|
726
|
+
}
|
|
727
|
+
var face = binding && binding.session;
|
|
728
|
+
if (!face) return "no-binding-for-session";
|
|
729
|
+
return typeof face.loadThrough === "function" ? "ready" : "no-loadThrough";
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// Structural narrowing of that projection (its value crosses the wire):
|
|
733
|
+
// `turn` and `seq` are load-bearing — an entry without them cannot be
|
|
734
|
+
// shown or jumped to — while the previews are decorative and degrade to "".
|
|
735
|
+
var outlineTurns = react.useMemo(function () {
|
|
736
|
+
var list = Array.isArray(rawOutline) ? rawOutline
|
|
737
|
+
: (rawOutline && Array.isArray(rawOutline.turns) ? rawOutline.turns : null);
|
|
738
|
+
if (!list) return null; // projection unavailable: loaded turns only
|
|
739
|
+
var out = [];
|
|
740
|
+
for (var i = 0; i < list.length; i++) {
|
|
741
|
+
var e = list[i];
|
|
742
|
+
if (!e || typeof e !== "object") continue;
|
|
743
|
+
if (typeof e.turn !== "number" || !isFinite(e.turn) || e.turn < 0) continue;
|
|
744
|
+
if (typeof e.seq !== "number" || !isFinite(e.seq) || e.seq < 0) continue;
|
|
745
|
+
out.push({
|
|
746
|
+
turn: e.turn,
|
|
747
|
+
seq: e.seq,
|
|
748
|
+
prompt: typeof e.prompt === "string" ? e.prompt : "",
|
|
749
|
+
response: typeof e.response === "string" ? e.response : ""
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return out;
|
|
753
|
+
}, [rawOutline]);
|
|
754
|
+
|
|
469
755
|
// ---- hooks (ALL before any conditional return) ----
|
|
470
756
|
// collapsed by default; user expands via the edge handle (default dock: left)
|
|
471
757
|
var _s1 = react.useState(false);
|
|
@@ -617,27 +903,43 @@ window.__ModuleLoader__.load({
|
|
|
617
903
|
try { localStorage.setItem(LEVELS_KEY, JSON.stringify(next)); } catch (e) {}
|
|
618
904
|
};
|
|
619
905
|
|
|
620
|
-
// ---- level
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
var
|
|
624
|
-
var
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
var
|
|
628
|
-
var
|
|
629
|
-
var
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
906
|
+
// ---- heading-level picker popup (transient): opens from the round button
|
|
907
|
+
// next to the search button; closes on an outside pointerdown. Closing
|
|
908
|
+
// plays a short shrink-back animation before unmounting. ----
|
|
909
|
+
var _sLo = react.useState(false);
|
|
910
|
+
var levelsOpen = _sLo[0];
|
|
911
|
+
var setLevelsOpen = _sLo[1];
|
|
912
|
+
var _sLc = react.useState(false);
|
|
913
|
+
var levelsClosing = _sLc[0];
|
|
914
|
+
var setLevelsClosing = _sLc[1];
|
|
915
|
+
var closeLevels = function () {
|
|
916
|
+
if (!levelsOpen || levelsClosing) return;
|
|
917
|
+
setLevelsClosing(true);
|
|
918
|
+
// unmount when the exit animation actually finishes (fallback timer in
|
|
919
|
+
// case animationend never fires, e.g. display:none ancestors)
|
|
920
|
+
var popped = document.querySelector(".dqt-levels-pop");
|
|
921
|
+
var done = false;
|
|
922
|
+
var finish = function () {
|
|
923
|
+
if (done) return;
|
|
924
|
+
done = true;
|
|
925
|
+
if (popped && popped.removeEventListener) popped.removeEventListener("animationend", finish);
|
|
926
|
+
setLevelsOpen(false);
|
|
927
|
+
setLevelsClosing(false);
|
|
928
|
+
};
|
|
929
|
+
if (popped && popped.addEventListener) popped.addEventListener("animationend", finish);
|
|
930
|
+
setTimeout(finish, 220);
|
|
633
931
|
};
|
|
932
|
+
react.useEffect(function () {
|
|
933
|
+
if (!levelsOpen) return;
|
|
934
|
+
var close = function (e) {
|
|
935
|
+
var t = e.target;
|
|
936
|
+
if (t && t.closest && (t.closest(".dqt-levels-pop") || t.closest(".dqt-levels-btn"))) return;
|
|
937
|
+
closeLevels();
|
|
938
|
+
};
|
|
939
|
+
document.addEventListener("pointerdown", close);
|
|
940
|
+
return function () { document.removeEventListener("pointerdown", close); };
|
|
941
|
+
}, [levelsOpen, levelsClosing]);
|
|
634
942
|
|
|
635
|
-
// ---- breadcrumb: the heading the reader is currently inside ----
|
|
636
|
-
var _sCrumb = react.useState(null);
|
|
637
|
-
var crumb = _sCrumb[0];
|
|
638
|
-
var setCrumb = _sCrumb[1];
|
|
639
|
-
var crumbRef = react.useRef(null);
|
|
640
|
-
var headingPathsRef = react.useRef({});
|
|
641
943
|
|
|
642
944
|
// ---- pagination: show the latest N groups; scrolling to the top loads older ----
|
|
643
945
|
var PAGE_SIZE = 6;
|
|
@@ -647,6 +949,79 @@ window.__ModuleLoader__.load({
|
|
|
647
949
|
var listRef = react.useRef(null);
|
|
648
950
|
var didInitScroll = react.useRef(false);
|
|
649
951
|
var outlineTouchRef = react.useRef(0); // last time the user touched the outline
|
|
952
|
+
var lastScrollTopRef = react.useRef(0);
|
|
953
|
+
var lastHostClickRef = react.useRef(0); // throttle for the "load earlier" click
|
|
954
|
+
|
|
955
|
+
// ---- "scroll up to load earlier messages" hint (search mode) -----------
|
|
956
|
+
// Armed by a search when older history is still reachable; dismissed the
|
|
957
|
+
// moment the reader scrolls up (which is when the older page starts loading,
|
|
958
|
+
// so the hint has done its job). "" | "more" | "oldest"
|
|
959
|
+
var _sHint = react.useState("");
|
|
960
|
+
var hint = _sHint[0];
|
|
961
|
+
var setHint = _sHint[1];
|
|
962
|
+
|
|
963
|
+
// ---- transient edge banner (outline mode) ------------------------------
|
|
964
|
+
// Reaching either end of the outline flashes a short bar over the bottom of
|
|
965
|
+
// the list: fade in, hold a few seconds, fade out. Deliberately NOT the same
|
|
966
|
+
// element as the search hint above (which stays until the reader acts).
|
|
967
|
+
var _sToast = react.useState(null);
|
|
968
|
+
var toast = _sToast[0];
|
|
969
|
+
var setToast = _sToast[1];
|
|
970
|
+
var toastTimersRef = react.useRef({ hold: null, out: null });
|
|
971
|
+
var showBanner = function (text) {
|
|
972
|
+
if (toastTimersRef.current.hold) clearTimeout(toastTimersRef.current.hold);
|
|
973
|
+
if (toastTimersRef.current.out) clearTimeout(toastTimersRef.current.out);
|
|
974
|
+
setToast({ text: text, closing: false });
|
|
975
|
+
toastTimersRef.current.hold = setTimeout(function () {
|
|
976
|
+
setToast(function (cur) { return cur === null ? null : { text: cur.text, closing: true }; });
|
|
977
|
+
toastTimersRef.current.out = setTimeout(function () { setToast(null); }, 460);
|
|
978
|
+
}, 2600);
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
// ---- hover preview card -------------------------------------------------
|
|
982
|
+
// Hovering an outline/result row opens a card with the section's opening
|
|
983
|
+
// lines; it lives in the body portal so the scrolling list cannot clip it.
|
|
984
|
+
var _sHov = react.useState(null);
|
|
985
|
+
var hoverCard = _sHov[0];
|
|
986
|
+
var setHoverCard = _sHov[1];
|
|
987
|
+
var _sHovOut = react.useState(false);
|
|
988
|
+
var hoverClosing = _sHovOut[0];
|
|
989
|
+
var setHoverClosing = _sHovOut[1];
|
|
990
|
+
var hoverTimerRef = react.useRef(null);
|
|
991
|
+
var hoverCloseTimerRef = react.useRef(null);
|
|
992
|
+
var hoverStart = function (info, el) {
|
|
993
|
+
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
994
|
+
hoverTimerRef.current = setTimeout(function () {
|
|
995
|
+
var r = el && el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
996
|
+
if (!r || r.width <= 0) return;
|
|
997
|
+
if (hoverCloseTimerRef.current) {
|
|
998
|
+
clearTimeout(hoverCloseTimerRef.current);
|
|
999
|
+
hoverCloseTimerRef.current = null;
|
|
1000
|
+
}
|
|
1001
|
+
setHoverClosing(false);
|
|
1002
|
+
setHoverCard({
|
|
1003
|
+
title: info.title,
|
|
1004
|
+
sub: info.sub,
|
|
1005
|
+
preview: info.preview,
|
|
1006
|
+
meta: info.meta,
|
|
1007
|
+
ghost: info.ghost,
|
|
1008
|
+
top: r.top,
|
|
1009
|
+
left: r.left,
|
|
1010
|
+
right: r.right
|
|
1011
|
+
});
|
|
1012
|
+
}, 260);
|
|
1013
|
+
};
|
|
1014
|
+
// leaving fades the card out instead of dropping it on the next frame
|
|
1015
|
+
var hoverEnd = function () {
|
|
1016
|
+
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
1017
|
+
if (hoverCloseTimerRef.current) clearTimeout(hoverCloseTimerRef.current);
|
|
1018
|
+
setHoverClosing(true);
|
|
1019
|
+
hoverCloseTimerRef.current = setTimeout(function () {
|
|
1020
|
+
hoverCloseTimerRef.current = null;
|
|
1021
|
+
setHoverCard(null);
|
|
1022
|
+
setHoverClosing(false);
|
|
1023
|
+
}, 240);
|
|
1024
|
+
};
|
|
650
1025
|
|
|
651
1026
|
// ---- the outline belongs to the CHAT view only: when the center column
|
|
652
1027
|
// switches to another view (trajectory / context / plugin views), fade the
|
|
@@ -718,6 +1093,21 @@ window.__ModuleLoader__.load({
|
|
|
718
1093
|
if (scopeTimerRef.current) clearTimeout(scopeTimerRef.current);
|
|
719
1094
|
scopeTimerRef.current = setTimeout(function () { setPrevScope(null); }, 300);
|
|
720
1095
|
};
|
|
1096
|
+
// ---- fuzzy matching: an independent switch beside the scope pill --------
|
|
1097
|
+
// Off (default) = the folded CONTIGUOUS match. On = subsequence matching, so
|
|
1098
|
+
// keywords tolerate text wedged in between ("模糊匹配" also finds "模糊的匹配").
|
|
1099
|
+
var FUZZY_KEY = "dsh-quick-toc.fuzzy.v1";
|
|
1100
|
+
var _sFz = react.useState(function () {
|
|
1101
|
+
try { return localStorage.getItem(FUZZY_KEY) === "1"; } catch (e) { return false; }
|
|
1102
|
+
});
|
|
1103
|
+
var fuzzy = _sFz[0];
|
|
1104
|
+
var setFuzzy = _sFz[1];
|
|
1105
|
+
var toggleFuzzy = function () {
|
|
1106
|
+
var next = !fuzzy;
|
|
1107
|
+
setFuzzy(next);
|
|
1108
|
+
setMatchIdx(0);
|
|
1109
|
+
try { localStorage.setItem(FUZZY_KEY, next ? "1" : "0"); } catch (e) {}
|
|
1110
|
+
};
|
|
721
1111
|
var openSearch = function () {
|
|
722
1112
|
setSearchOpen(true);
|
|
723
1113
|
setSearchAnim("enter");
|
|
@@ -773,9 +1163,9 @@ window.__ModuleLoader__.load({
|
|
|
773
1163
|
}
|
|
774
1164
|
return false;
|
|
775
1165
|
};
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1166
|
+
var viaHost = function () {
|
|
1167
|
+
var btn = findLoadOlderButton();
|
|
1168
|
+
if (!btn || btn.disabled) return false;
|
|
779
1169
|
btn.click();
|
|
780
1170
|
// once the conversation loads more, expand the outline window too
|
|
781
1171
|
setTimeout(function () {
|
|
@@ -788,7 +1178,34 @@ window.__ModuleLoader__.load({
|
|
|
788
1178
|
});
|
|
789
1179
|
}
|
|
790
1180
|
}, 1200);
|
|
791
|
-
|
|
1181
|
+
return true;
|
|
1182
|
+
};
|
|
1183
|
+
var viaHostThrottled = function () {
|
|
1184
|
+
var now = Date.now();
|
|
1185
|
+
if (now - lastHostClickRef.current < 900) return false;
|
|
1186
|
+
if (!findLoadOlderButton()) return false;
|
|
1187
|
+
lastHostClickRef.current = now;
|
|
1188
|
+
return viaHost();
|
|
1189
|
+
};
|
|
1190
|
+
// In SEARCH mode the list shows matches, not the outline: paging the
|
|
1191
|
+
// outline's own window would change nothing the reader can see, whereas
|
|
1192
|
+
// loading the conversation's older messages extends what the search can
|
|
1193
|
+
// cover at all (the whole-log index only carries bounded previews). So the
|
|
1194
|
+
// host's own "load earlier" comes first while a query is active.
|
|
1195
|
+
if (query.trim() !== "") return viaHost() || grow();
|
|
1196
|
+
// Outline mode keeps the ORIGINAL contract: scrolling the outline up also
|
|
1197
|
+
// pulls the conversation's older messages in (that is what makes the outline
|
|
1198
|
+
// a substitute for scrolling the transcript), not merely more index entries.
|
|
1199
|
+
// The host click is throttled so one continuous scroll cannot hammer the
|
|
1200
|
+
// pager; grow() still runs first so the index never lags behind the window.
|
|
1201
|
+
var grew = grow();
|
|
1202
|
+
return viaHostThrottled() || grew;
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
// older history still reachable? the outline can always page its own window
|
|
1206
|
+
// further, and beyond that the conversation's own "load older" button decides
|
|
1207
|
+
var canLoadOlder = function () {
|
|
1208
|
+
return visibleCount < groups.length || !!findLoadOlderButton();
|
|
792
1209
|
};
|
|
793
1210
|
|
|
794
1211
|
// wheel up (toward older) loads more when the list is at its top or has
|
|
@@ -796,17 +1213,52 @@ window.__ModuleLoader__.load({
|
|
|
796
1213
|
// always refreshes older turns, even without a visible scrollbar
|
|
797
1214
|
var onListWheel = function (e) {
|
|
798
1215
|
outlineTouchRef.current = Date.now();
|
|
799
|
-
if (e.deltaY >= 0) return;
|
|
800
1216
|
var el = listRef.current;
|
|
801
1217
|
if (!el) return;
|
|
802
|
-
|
|
1218
|
+
hoverEnd();
|
|
1219
|
+
if (e.deltaY >= 0) {
|
|
1220
|
+
// still scrolling down at the very end of the outline: say so. Outline
|
|
1221
|
+
// mode only — the search list has its own persistent hint line.
|
|
1222
|
+
var max = el.scrollHeight - el.clientHeight;
|
|
1223
|
+
if (max > 4 && el.scrollTop >= max - 4 && query.trim() === "") showBanner(T("hint.bottom"));
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (hint === "more") setHint(""); // scrolling up is what the hint asked for
|
|
1227
|
+
if (el.scrollTop <= 1) {
|
|
1228
|
+
var progressed = loadOlderOutline(el);
|
|
1229
|
+
if (!progressed) {
|
|
1230
|
+
if (query.trim() !== "") {
|
|
1231
|
+
if (hint !== "oldest") setHint("oldest");
|
|
1232
|
+
} else {
|
|
1233
|
+
showBanner(T("search.hintOldest"));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
803
1237
|
};
|
|
804
1238
|
|
|
805
1239
|
// scroll to the top edge also loads older groups (keeps the visual position)
|
|
806
1240
|
var onListScroll = function (e) {
|
|
807
1241
|
outlineTouchRef.current = Date.now();
|
|
808
1242
|
var el = e.currentTarget;
|
|
809
|
-
|
|
1243
|
+
var upward = el.scrollTop < lastScrollTopRef.current - 2;
|
|
1244
|
+
if (upward) {
|
|
1245
|
+
if (hint === "more") setHint("");
|
|
1246
|
+
if (hoverCard) hoverEnd();
|
|
1247
|
+
}
|
|
1248
|
+
lastScrollTopRef.current = el.scrollTop;
|
|
1249
|
+
if (el.scrollTop <= 24) {
|
|
1250
|
+
// Reaching the top is the ONLY place the exhausted case can be told
|
|
1251
|
+
// apart from "keep scrolling": after a search the list is parked at the
|
|
1252
|
+
// bottom, so the first wheel-up merely scrolls and never gets here.
|
|
1253
|
+
var progressed = loadOlderOutline(el);
|
|
1254
|
+
if (!progressed && upward) {
|
|
1255
|
+
if (query.trim() !== "") {
|
|
1256
|
+
if (hint !== "oldest") setHint("oldest");
|
|
1257
|
+
} else {
|
|
1258
|
+
showBanner(T("search.hintOldest"));
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
810
1262
|
};
|
|
811
1263
|
|
|
812
1264
|
// ---- resize drags (right edge = width, bottom edge = height) ----
|
|
@@ -961,14 +1413,53 @@ window.__ModuleLoader__.load({
|
|
|
961
1413
|
var info = nodeInfo(key, node);
|
|
962
1414
|
current.msgs.push({ key: key, text: info.text });
|
|
963
1415
|
for (var k = 0; k < info.headings.length; k++) {
|
|
964
|
-
current.headings.push({
|
|
1416
|
+
current.headings.push({
|
|
1417
|
+
level: info.headings[k].level,
|
|
1418
|
+
title: info.headings[k].title,
|
|
1419
|
+
key: key,
|
|
1420
|
+
idx: k,
|
|
1421
|
+
sub: info.headings[k].sub,
|
|
1422
|
+
preview: info.headings[k].preview
|
|
1423
|
+
});
|
|
965
1424
|
}
|
|
966
1425
|
}
|
|
967
1426
|
pruneNodeInfo(seen);
|
|
968
1427
|
// keep a turn when it has headings OR a time — turns without headings
|
|
969
1428
|
// still get a standalone time entry in the outline (click to jump)
|
|
970
|
-
|
|
971
|
-
|
|
1429
|
+
var ready = result.filter(function (g) { return g.headings.length > 0 || g.time !== ""; });
|
|
1430
|
+
if (outlineTurns === null) return ready;
|
|
1431
|
+
// ---- merge the whole-log turn index ---------------------------------
|
|
1432
|
+
// Loaded groups keep their order (a group whose node carries no turn
|
|
1433
|
+
// location stays where it was found); every turn the paged window has not
|
|
1434
|
+
// loaded yet becomes an "unloaded" entry in the gap where it belongs, so
|
|
1435
|
+
// the outline covers the whole session and heading-less turns stay
|
|
1436
|
+
// reachable by number. Preview text comes from the projection and is
|
|
1437
|
+
// bounded by it (host-side 50/120 character budgets).
|
|
1438
|
+
var merged = [];
|
|
1439
|
+
var li = 0;
|
|
1440
|
+
for (var ei = 0; ei < outlineTurns.length; ei++) {
|
|
1441
|
+
var entry = outlineTurns[ei];
|
|
1442
|
+
while (li < ready.length && (ready[li].turn === null || ready[li].turn < entry.turn)) {
|
|
1443
|
+
merged.push(ready[li]);
|
|
1444
|
+
li++;
|
|
1445
|
+
}
|
|
1446
|
+
if (li < ready.length && ready[li].turn === entry.turn) continue; // emitted above
|
|
1447
|
+
merged.push({
|
|
1448
|
+
turn: entry.turn,
|
|
1449
|
+
seq: entry.seq,
|
|
1450
|
+
ghost: true,
|
|
1451
|
+
time: "",
|
|
1452
|
+
userKey: "",
|
|
1453
|
+
userText: entry.prompt,
|
|
1454
|
+
userFull: "",
|
|
1455
|
+
msgs: [],
|
|
1456
|
+
headings: [],
|
|
1457
|
+
preview: entry.response
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
while (li < ready.length) { merged.push(ready[li]); li++; }
|
|
1461
|
+
return merged;
|
|
1462
|
+
}, [order, nodes, outlineTurns]);
|
|
972
1463
|
// first time content appears, scroll the list to the bottom (newest).
|
|
973
1464
|
// NOTE: must stay BELOW the `groups` memo — a deps array is evaluated
|
|
974
1465
|
// during render, so reading `groups` before that `var` is assigned would
|
|
@@ -978,14 +1469,25 @@ window.__ModuleLoader__.load({
|
|
|
978
1469
|
didInitScroll.current = true;
|
|
979
1470
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
980
1471
|
}, [groups.length]);
|
|
1472
|
+
// one-line diagnostic so the loaded build AND the host capabilities it can
|
|
1473
|
+
// actually reach are identifiable from the browser console
|
|
1474
|
+
var mountLogRef = react.useRef(false);
|
|
1475
|
+
react.useEffect(function () {
|
|
1476
|
+
if (mountLogRef.current) return;
|
|
1477
|
+
mountLogRef.current = true;
|
|
1478
|
+
console.log("[dsh-quick-toc] panel mounted · turnOutline=" +
|
|
1479
|
+
(outlineTurns ? outlineTurns.length + " turns" : "unavailable") +
|
|
1480
|
+
" · jumpLoader=" + jumpLoaderState());
|
|
1481
|
+
}, [outlineTurns]);
|
|
1482
|
+
|
|
981
1483
|
var groupTrees = react.useMemo(function () {
|
|
982
1484
|
return groups.map(function (g) {
|
|
983
|
-
if (levels.length === 6) return buildTree(g.headings);
|
|
1485
|
+
if (levels.length === 6) return buildTree(g.headings, g.time);
|
|
984
1486
|
var kept = [];
|
|
985
1487
|
for (var i = 0; i < g.headings.length; i++) {
|
|
986
1488
|
if (levelSet[g.headings[i].level]) kept.push(g.headings[i]);
|
|
987
1489
|
}
|
|
988
|
-
return buildTree(kept);
|
|
1490
|
+
return buildTree(kept, g.time);
|
|
989
1491
|
});
|
|
990
1492
|
}, [groups, levels, levelSet]);
|
|
991
1493
|
// pagination slice: the latest `visibleCount` groups
|
|
@@ -993,18 +1495,28 @@ window.__ModuleLoader__.load({
|
|
|
993
1495
|
var shownTrees = groupTrees.slice(groupTrees.length - shownGroups.length);
|
|
994
1496
|
|
|
995
1497
|
// the group currently being read (the turn under the middle of the
|
|
996
|
-
// CONVERSATION viewport) stays bright in the outline; others are dimmed
|
|
1498
|
+
// CONVERSATION viewport) stays bright in the outline; others are dimmed.
|
|
1499
|
+
// Every node key of every loaded group -> its group index: mapping ONLY the
|
|
1500
|
+
// heading-bearing messages meant a turn with no markdown headings had no
|
|
1501
|
+
// entry at all, so a jump that landed on it (the host's own turn rail, for
|
|
1502
|
+
// one) could never light up or follow in the outline.
|
|
997
1503
|
var keyToGroup = react.useMemo(function () {
|
|
998
1504
|
var m = {};
|
|
999
1505
|
for (var i = 0; i < groups.length; i++) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1506
|
+
var g = groups[i];
|
|
1507
|
+
if (g.ghost) continue; // no loaded rows to match
|
|
1508
|
+
if (g.userKey) m[g.userKey] = i;
|
|
1509
|
+
for (var j = 0; j < g.headings.length; j++) {
|
|
1510
|
+
m[g.headings[j].key] = i;
|
|
1511
|
+
}
|
|
1512
|
+
for (var k = 0; k < g.msgs.length; k++) {
|
|
1513
|
+
m[g.msgs[k].key] = i;
|
|
1002
1514
|
}
|
|
1003
1515
|
}
|
|
1004
1516
|
return m;
|
|
1005
1517
|
}, [groups]);
|
|
1006
1518
|
|
|
1007
|
-
// ancestor path of every heading occurrence (
|
|
1519
|
+
// ancestor path of every heading occurrence (used by the search result rows):
|
|
1008
1520
|
// id -> { path, title, level, gi }
|
|
1009
1521
|
var headingPaths = react.useMemo(function () {
|
|
1010
1522
|
var byId = {};
|
|
@@ -1029,8 +1541,9 @@ window.__ModuleLoader__.load({
|
|
|
1029
1541
|
// search result rows: one row per matched heading/message (with an
|
|
1030
1542
|
// occurrence count), carrying its heading path, turn time and a snippet.
|
|
1031
1543
|
var resultRows = react.useMemo(function () {
|
|
1032
|
-
var
|
|
1033
|
-
if (!
|
|
1544
|
+
var fq = foldQuery(query);
|
|
1545
|
+
if (!fq) return [];
|
|
1546
|
+
var needle = { folded: fq }; // one folded query, reused for every text
|
|
1034
1547
|
var rows = [];
|
|
1035
1548
|
var index = {};
|
|
1036
1549
|
var push = function (row, n) {
|
|
@@ -1043,44 +1556,83 @@ window.__ModuleLoader__.load({
|
|
|
1043
1556
|
};
|
|
1044
1557
|
for (var gi = 0; gi < groups.length; gi++) {
|
|
1045
1558
|
var g = groups[gi];
|
|
1559
|
+
// a turn the paged window has not loaded: searchable through the host
|
|
1560
|
+
// outline's bounded previews, and clicking it pages the turn in first
|
|
1561
|
+
if (g.ghost) {
|
|
1562
|
+
if (searchScope === "full") {
|
|
1563
|
+
var gtext = (g.userText || "") + "\n" + (g.preview || "");
|
|
1564
|
+
var ng = countOccurrences(gtext, needle, fuzzy);
|
|
1565
|
+
if (ng > 0) {
|
|
1566
|
+
push({
|
|
1567
|
+
gi: gi, key: "", idx: undefined,
|
|
1568
|
+
title: previewText(g.userText || g.preview || "", 40),
|
|
1569
|
+
level: 0, path: "", time: "",
|
|
1570
|
+
snippet: snippetAround(g.preview || g.userText || "", fq, 36, fuzzy),
|
|
1571
|
+
ghost: true, turn: g.turn, seq: g.seq
|
|
1572
|
+
}, ng);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1046
1577
|
for (var j = 0; j < g.headings.length; j++) {
|
|
1047
1578
|
var h = g.headings[j];
|
|
1048
|
-
var nh = countOccurrences(h.title,
|
|
1579
|
+
var nh = countOccurrences(h.title, needle, fuzzy);
|
|
1049
1580
|
if (nh > 0) {
|
|
1050
1581
|
var hp = headingPaths[headingId(h)];
|
|
1051
1582
|
push({
|
|
1052
1583
|
gi: gi, key: h.key, idx: h.idx, title: h.title, level: h.level,
|
|
1053
|
-
|
|
1584
|
+
// the subtitle identifies same-titled headings ("which one?")
|
|
1585
|
+
path: hp ? hp.path : "", time: g.time, snippet: h.sub || ""
|
|
1054
1586
|
}, nh);
|
|
1055
1587
|
}
|
|
1056
1588
|
}
|
|
1057
1589
|
if (searchScope === "full") {
|
|
1058
|
-
var nu = g.userKey && g.userFull ? countOccurrences(g.userFull,
|
|
1590
|
+
var nu = g.userKey && g.userFull ? countOccurrences(g.userFull, needle, fuzzy) : 0;
|
|
1059
1591
|
if (nu > 0) {
|
|
1060
1592
|
push({
|
|
1061
1593
|
gi: gi, key: g.userKey, idx: undefined, title: previewText(g.userFull, 40),
|
|
1062
|
-
level: 0, path: "", time: g.time, snippet: snippetAround(g.userFull,
|
|
1594
|
+
level: 0, path: "", time: g.time, snippet: snippetAround(g.userFull, fq, 36, fuzzy)
|
|
1063
1595
|
}, nu);
|
|
1064
1596
|
}
|
|
1065
1597
|
for (var m = 0; m < g.msgs.length; m++) {
|
|
1066
1598
|
var msg = g.msgs[m];
|
|
1067
1599
|
if (!msg.text) continue;
|
|
1068
|
-
var nm = countOccurrences(msg.text,
|
|
1600
|
+
var nm = countOccurrences(msg.text, needle, fuzzy);
|
|
1069
1601
|
if (nm === 0) continue;
|
|
1070
1602
|
push({
|
|
1071
1603
|
gi: gi, key: msg.key, idx: undefined, title: previewText(msg.text, 40),
|
|
1072
|
-
level: 0, path: "", time: g.time, snippet: snippetAround(msg.text,
|
|
1604
|
+
level: 0, path: "", time: g.time, snippet: snippetAround(msg.text, fq, 36, fuzzy)
|
|
1073
1605
|
}, nm);
|
|
1074
1606
|
}
|
|
1075
1607
|
}
|
|
1076
1608
|
}
|
|
1077
1609
|
return rows;
|
|
1078
|
-
}, [groups, query, searchScope, headingPaths]);
|
|
1079
|
-
|
|
1610
|
+
}, [groups, query, searchScope, headingPaths, fuzzy]);
|
|
1611
|
+
// ---- auto-follow: which turn is the reader looking at -------------------
|
|
1612
|
+
// This used to hang off ONE scroll listener bound to the scrollport element
|
|
1613
|
+
// captured when the effect ran. DSH can replace that element (a jump that
|
|
1614
|
+
// repages the window remounts the list), and the listener then stays on the
|
|
1615
|
+
// detached node — the outline silently stops following. So it re-queries the
|
|
1616
|
+
// scrollport on every run, listens on the DOCUMENT in the capture phase
|
|
1617
|
+
// (scroll does not bubble; capture still sees every descendant), and keeps a
|
|
1618
|
+
// slow poll for programmatic anchor adjustments that emit no scroll event.
|
|
1619
|
+
//
|
|
1620
|
+
// The outline's own scroll is INSTANT and starts on the next frame: a following
|
|
1621
|
+
// rail that animates its way to the target reads as permanently one step behind.
|
|
1080
1622
|
react.useEffect(function () {
|
|
1081
|
-
var
|
|
1082
|
-
|
|
1083
|
-
var
|
|
1623
|
+
var followFrame = null;
|
|
1624
|
+
var followTimer = null;
|
|
1625
|
+
var lastRun = 0;
|
|
1626
|
+
var update = function (force) {
|
|
1627
|
+
// scroll frames are hot and a document-level capture listener sees every
|
|
1628
|
+
// scroll in the app; the rail only changes when a different turn crosses
|
|
1629
|
+
// the viewport, so cap the row measurement at ~16/s
|
|
1630
|
+
var now = Date.now();
|
|
1631
|
+
if (!force && now - lastRun < 60) return;
|
|
1632
|
+
lastRun = now;
|
|
1633
|
+
if (!chatViewRef.current) return;
|
|
1634
|
+
var sp = document.querySelector("[data-conversation-scroll]");
|
|
1635
|
+
if (!sp) return;
|
|
1084
1636
|
var lr = sp.getBoundingClientRect();
|
|
1085
1637
|
var vTop = lr.top;
|
|
1086
1638
|
var vBottom = lr.top + lr.height;
|
|
@@ -1096,74 +1648,43 @@ window.__ModuleLoader__.load({
|
|
|
1096
1648
|
}
|
|
1097
1649
|
}
|
|
1098
1650
|
var sig = actives.slice().sort().join(",");
|
|
1099
|
-
if (sig
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}, 120);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
// breadcrumb: the heading of the section that occupies most of the
|
|
1127
|
-
// viewport — i.e. the deepest heading above the viewport's MIDDLE line.
|
|
1128
|
-
// Walking backwards lets a heading we cannot map (e.g. one inside a tool
|
|
1129
|
-
// result) fall back to the nearest mapped ancestor heading.
|
|
1130
|
-
//
|
|
1131
|
-
// The middle line is what makes this consistent with every jump: a jump
|
|
1132
|
-
// (outline click, breadcrumb click, match stepping) always lands its
|
|
1133
|
-
// target ABOVE the middle, so clicking the breadcrumb never changes
|
|
1134
|
-
// which heading the breadcrumb shows.
|
|
1135
|
-
var crumbLine = vTop + lr.height * 0.5;
|
|
1136
|
-
var heads = sp.querySelectorAll("h1,h2,h3,h4,h5,h6");
|
|
1137
|
-
var nextCrumb = null;
|
|
1138
|
-
var paths = headingPathsRef.current;
|
|
1139
|
-
for (var hi = heads.length - 1; hi >= 0; hi--) {
|
|
1140
|
-
var hr = heads[hi].getBoundingClientRect();
|
|
1141
|
-
if (hr.top > crumbLine) continue;
|
|
1142
|
-
var rowEl = heads[hi].closest ? heads[hi].closest("[data-chat-anchor-key]") : null;
|
|
1143
|
-
if (!rowEl || !rowEl.dataset) continue;
|
|
1144
|
-
var rowKey = rowEl.dataset.chatAnchorKey;
|
|
1145
|
-
var rowHeads = rowEl.querySelectorAll("h1,h2,h3,h4,h5,h6");
|
|
1146
|
-
var idxInRow = -1;
|
|
1147
|
-
for (var rh = 0; rh < rowHeads.length; rh++) {
|
|
1148
|
-
if (rowHeads[rh] === heads[hi]) { idxInRow = rh; break; }
|
|
1149
|
-
}
|
|
1150
|
-
var meta = idxInRow >= 0 ? paths[rowKey + "#" + idxInRow] : undefined;
|
|
1151
|
-
if (meta !== undefined) {
|
|
1152
|
-
nextCrumb = { path: meta.path, key: rowKey, idx: idxInRow };
|
|
1153
|
-
break;
|
|
1651
|
+
if (sig === activeSigRef.current) return;
|
|
1652
|
+
activeSigRef.current = sig;
|
|
1653
|
+
setActiveGroup(actives);
|
|
1654
|
+
if (actives.length === 0) return;
|
|
1655
|
+
// pause right after the reader touched the outline themselves —
|
|
1656
|
+
// otherwise paging older turns gets yanked back to the newest group
|
|
1657
|
+
if (Date.now() - outlineTouchRef.current <= 1500) return;
|
|
1658
|
+
var gi0 = actives[0];
|
|
1659
|
+
setVisibleCount(function (prev) {
|
|
1660
|
+
var need = groups.length - gi0 + 3;
|
|
1661
|
+
return Math.max(prev, Math.min(groups.length, need));
|
|
1662
|
+
});
|
|
1663
|
+
if (followFrame) cancelAnimationFrame(followFrame);
|
|
1664
|
+
if (followTimer) clearTimeout(followTimer);
|
|
1665
|
+
var place = function () {
|
|
1666
|
+
var el = listRef.current;
|
|
1667
|
+
if (!el) return;
|
|
1668
|
+
var node = el.querySelector('[data-group-idx="' + gi0 + '"]');
|
|
1669
|
+
if (!node) return;
|
|
1670
|
+
var er = node.getBoundingClientRect();
|
|
1671
|
+
var lr2 = el.getBoundingClientRect();
|
|
1672
|
+
if (er.top < lr2.top - 2 || er.bottom > lr2.bottom + 2) {
|
|
1673
|
+
el.scrollTop = el.scrollTop + (er.top - lr2.top) - el.clientHeight / 2 + node.offsetHeight / 2;
|
|
1154
1674
|
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1675
|
+
};
|
|
1676
|
+
followFrame = requestAnimationFrame(function () { followFrame = requestAnimationFrame(place); });
|
|
1677
|
+
followTimer = setTimeout(place, 90); // the row may only exist after the expansion commits
|
|
1678
|
+
};
|
|
1679
|
+
update(true);
|
|
1680
|
+
document.addEventListener("scroll", update, { passive: true, capture: true });
|
|
1681
|
+
var poll = setInterval(function () { update(true); }, 250);
|
|
1682
|
+
return function () {
|
|
1683
|
+
document.removeEventListener("scroll", update, { capture: true });
|
|
1684
|
+
clearInterval(poll);
|
|
1685
|
+
if (followFrame) cancelAnimationFrame(followFrame);
|
|
1686
|
+
if (followTimer) clearTimeout(followTimer);
|
|
1163
1687
|
};
|
|
1164
|
-
update();
|
|
1165
|
-
sp.addEventListener("scroll", update, { passive: true });
|
|
1166
|
-
return function () { sp.removeEventListener("scroll", update); };
|
|
1167
1688
|
}, [keyToGroup]);
|
|
1168
1689
|
|
|
1169
1690
|
// ---- search matches ----
|
|
@@ -1172,21 +1693,32 @@ window.__ModuleLoader__.load({
|
|
|
1172
1693
|
// Every occurrence counts (multiple hits inside one message = multiple
|
|
1173
1694
|
// matches), so the n/N counter reflects the real total.
|
|
1174
1695
|
var matches = react.useMemo(function () {
|
|
1175
|
-
var
|
|
1176
|
-
if (!
|
|
1696
|
+
var fq = foldQuery(query);
|
|
1697
|
+
if (!fq) return [];
|
|
1698
|
+
var needle = { folded: fq };
|
|
1177
1699
|
var out = [];
|
|
1178
1700
|
for (var gi = 0; gi < groups.length; gi++) {
|
|
1179
1701
|
var g = groups[gi];
|
|
1702
|
+
if (g.ghost) {
|
|
1703
|
+
if (searchScope === "full") {
|
|
1704
|
+
var gtext = (g.userText || "") + "\n" + (g.preview || "");
|
|
1705
|
+
var ng = countOccurrences(gtext, needle, fuzzy);
|
|
1706
|
+
for (var cg = 0; cg < ng; cg++) {
|
|
1707
|
+
out.push({ gi: gi, title: g.userText || "", key: "", idx: undefined, ghost: true, turn: g.turn, seq: g.seq });
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1180
1712
|
for (var j = 0; j < g.headings.length; j++) {
|
|
1181
1713
|
var h = g.headings[j];
|
|
1182
|
-
var n = countOccurrences(h.title,
|
|
1714
|
+
var n = countOccurrences(h.title, needle, fuzzy);
|
|
1183
1715
|
for (var c = 0; c < n; c++) {
|
|
1184
1716
|
out.push({ gi: gi, title: h.title, key: h.key, idx: h.idx });
|
|
1185
1717
|
}
|
|
1186
1718
|
}
|
|
1187
1719
|
if (searchScope === "full") {
|
|
1188
1720
|
if (g.userKey && g.userFull) {
|
|
1189
|
-
var nu = countOccurrences(g.userFull,
|
|
1721
|
+
var nu = countOccurrences(g.userFull, needle, fuzzy);
|
|
1190
1722
|
for (var cu = 0; cu < nu; cu++) {
|
|
1191
1723
|
out.push({ gi: gi, title: previewText(g.userFull, 30), key: g.userKey, idx: undefined });
|
|
1192
1724
|
}
|
|
@@ -1194,7 +1726,7 @@ window.__ModuleLoader__.load({
|
|
|
1194
1726
|
for (var m = 0; m < g.msgs.length; m++) {
|
|
1195
1727
|
var msg = g.msgs[m];
|
|
1196
1728
|
if (!msg.text) continue;
|
|
1197
|
-
var nm = countOccurrences(msg.text,
|
|
1729
|
+
var nm = countOccurrences(msg.text, needle, fuzzy);
|
|
1198
1730
|
for (var cm = 0; cm < nm; cm++) {
|
|
1199
1731
|
out.push({ gi: gi, title: previewText(msg.text, 30), key: msg.key, idx: undefined });
|
|
1200
1732
|
}
|
|
@@ -1202,7 +1734,7 @@ window.__ModuleLoader__.load({
|
|
|
1202
1734
|
}
|
|
1203
1735
|
}
|
|
1204
1736
|
return out;
|
|
1205
|
-
}, [groups, query, searchScope]);
|
|
1737
|
+
}, [groups, query, searchScope, fuzzy]);
|
|
1206
1738
|
|
|
1207
1739
|
// which result row holds the current (n/N) occurrence (needs `matches`)
|
|
1208
1740
|
var activeResultRow = -1;
|
|
@@ -1225,10 +1757,13 @@ window.__ModuleLoader__.load({
|
|
|
1225
1757
|
var q = query.trim();
|
|
1226
1758
|
if (searchPosRef.current === q) return;
|
|
1227
1759
|
searchPosRef.current = q;
|
|
1228
|
-
if (!q) return;
|
|
1760
|
+
if (!q) { setHint(""); return; }
|
|
1229
1761
|
var el = listRef.current;
|
|
1230
1762
|
if (el) el.scrollTop = el.scrollHeight;
|
|
1231
1763
|
if (matches.length > 0) setMatchIdx(matches.length - 1);
|
|
1764
|
+
// arm the "scroll up for older messages" hint: the result list only covers
|
|
1765
|
+
// what the window holds, and older turns are one upward scroll away
|
|
1766
|
+
setHint(canLoadOlder() ? "more" : "");
|
|
1232
1767
|
}, [query, matches.length]);
|
|
1233
1768
|
|
|
1234
1769
|
// keep the current row in view while stepping with Enter
|
|
@@ -1299,15 +1834,8 @@ window.__ModuleLoader__.load({
|
|
|
1299
1834
|
// ---- jump: smooth glide to the exact heading element. When glued to the
|
|
1300
1835
|
// bottom, lift just past DSH's 25px stick-to-bottom threshold first so
|
|
1301
1836
|
// the glide is not yanked back. ----
|
|
1302
|
-
var
|
|
1303
|
-
var
|
|
1304
|
-
if (!row) return;
|
|
1305
|
-
var el = row;
|
|
1306
|
-
if (idx !== undefined && idx !== null) {
|
|
1307
|
-
var hs = row.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
|
1308
|
-
if (hs.length > 0) el = hs[Math.min(idx, hs.length - 1)] || row;
|
|
1309
|
-
}
|
|
1310
|
-
var sp = row.closest ? row.closest("[data-conversation-scroll]") : null;
|
|
1837
|
+
var glideTo = function (el) {
|
|
1838
|
+
var sp = el.closest ? el.closest("[data-conversation-scroll]") : null;
|
|
1311
1839
|
if (!sp) {
|
|
1312
1840
|
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1313
1841
|
return;
|
|
@@ -1320,6 +1848,104 @@ window.__ModuleLoader__.load({
|
|
|
1320
1848
|
sp.scrollTo({ top: t, behavior: "smooth" });
|
|
1321
1849
|
};
|
|
1322
1850
|
|
|
1851
|
+
var jump = function (key, idx) {
|
|
1852
|
+
var row = findRow(key);
|
|
1853
|
+
if (!row) return;
|
|
1854
|
+
var el = row;
|
|
1855
|
+
if (idx !== undefined && idx !== null) {
|
|
1856
|
+
var hs = row.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
|
1857
|
+
if (hs.length > 0) el = hs[Math.min(idx, hs.length - 1)] || row;
|
|
1858
|
+
}
|
|
1859
|
+
glideTo(el);
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
// ---- unloaded turns: page history to the turn, then land on it ----------
|
|
1863
|
+
// The host turn outline carries each turn's `turn/start` seq and the session
|
|
1864
|
+
// face's loadThrough(seq) is the documented jump loader ("page history
|
|
1865
|
+
// backwards until the window covers seq"). Once it resolves the turn's nodes
|
|
1866
|
+
// exist, so we poll briefly for its row — the native turn rail waits for the
|
|
1867
|
+
// same settle.
|
|
1868
|
+
var turnRow = function (turn) {
|
|
1869
|
+
try {
|
|
1870
|
+
return document.querySelector('[data-chat-turn="' + String(turn) + '"]');
|
|
1871
|
+
} catch (e) {
|
|
1872
|
+
return null;
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1876
|
+
// Fallback locator for the poll below: once the paged window covers the
|
|
1877
|
+
// turn, OUR OWN rebuilt outline has a loaded group for it, which carries the
|
|
1878
|
+
// anchor keys of that turn's nodes. That path does not depend on the host
|
|
1879
|
+
// keeping a `data-chat-turn` attribute on the row.
|
|
1880
|
+
var groupsRef = react.useRef([]);
|
|
1881
|
+
react.useEffect(function () { groupsRef.current = groups; }, [groups]);
|
|
1882
|
+
var loadedGroupRow = function (turn) {
|
|
1883
|
+
var list = groupsRef.current || [];
|
|
1884
|
+
for (var i = 0; i < list.length; i++) {
|
|
1885
|
+
var g = list[i];
|
|
1886
|
+
if (g.ghost || g.turn !== turn) continue;
|
|
1887
|
+
var key = (g.msgs && g.msgs.length > 0) ? g.msgs[0].key : g.userKey;
|
|
1888
|
+
var row = key ? findRow(key) : null;
|
|
1889
|
+
if (row) return row;
|
|
1890
|
+
}
|
|
1891
|
+
return null;
|
|
1892
|
+
};
|
|
1893
|
+
|
|
1894
|
+
var landOnTurn = function (turn) {
|
|
1895
|
+
var tries = 0;
|
|
1896
|
+
var step = function () {
|
|
1897
|
+
tries++;
|
|
1898
|
+
var row = turnRow(turn) || loadedGroupRow(turn);
|
|
1899
|
+
if (row) {
|
|
1900
|
+
glideTo(row);
|
|
1901
|
+
// a jump that started from a search hit keeps the hit highlighted
|
|
1902
|
+
if (query.trim()) {
|
|
1903
|
+
clearHighlights();
|
|
1904
|
+
highlightRow(row, query.trim(), 0, fuzzy);
|
|
1905
|
+
}
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (tries < 25) setTimeout(step, 80);
|
|
1909
|
+
};
|
|
1910
|
+
setTimeout(step, 40);
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
// returns false when the host exposes no jump loader (older DSH builds)
|
|
1914
|
+
var openTurn = function (turn, seq) {
|
|
1915
|
+
var sessions = hostSessions();
|
|
1916
|
+
if (!sessions) {
|
|
1917
|
+
console.warn("[dsh-quick-toc] ctx.get(\"sessions\") returned nothing: this host has no session service, cannot open turn " + turn);
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
var binding = typeof sessions.binding === "function" ? sessions.binding(sessionId) : null;
|
|
1921
|
+
var face = binding && binding.session;
|
|
1922
|
+
if (!face || typeof face.loadThrough !== "function") {
|
|
1923
|
+
console.warn("[dsh-quick-toc] session " + String(sessionId) + " exposes no loadThrough jump loader, cannot open turn " + turn);
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
setHoverCard(null);
|
|
1927
|
+
Promise.resolve(face.loadThrough(seq)).then(function () {
|
|
1928
|
+
landOnTurn(turn);
|
|
1929
|
+
}, function (err) {
|
|
1930
|
+
console.warn("[dsh-quick-toc] loadThrough failed:", err);
|
|
1931
|
+
});
|
|
1932
|
+
return true;
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
// reveal one group inside the outline list (shared by Enter-stepping and by
|
|
1936
|
+
// jumps into a turn that had to be loaded first)
|
|
1937
|
+
var revealGroupInOutline = function (gi) {
|
|
1938
|
+
setVisibleCount(function (prev) {
|
|
1939
|
+
return Math.max(prev, Math.min(groups.length, groups.length - gi));
|
|
1940
|
+
});
|
|
1941
|
+
setTimeout(function () {
|
|
1942
|
+
var el = listRef.current;
|
|
1943
|
+
if (!el) return;
|
|
1944
|
+
var node = el.querySelector('[data-group-idx="' + gi + '"]');
|
|
1945
|
+
if (node) node.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1946
|
+
}, 150);
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1323
1949
|
// jump to the n-th match, cycling; also reveal the group in the outline.
|
|
1324
1950
|
// ONE smooth scroll straight to the current occurrence (no competing
|
|
1325
1951
|
// scrolls): highlight first, then position the mark at the upper-middle
|
|
@@ -1328,14 +1954,21 @@ window.__ModuleLoader__.load({
|
|
|
1328
1954
|
var i = ((n % matches.length) + matches.length) % matches.length;
|
|
1329
1955
|
setMatchIdx(i);
|
|
1330
1956
|
var m = matches[i];
|
|
1331
|
-
var q = query.trim()
|
|
1957
|
+
var q = query.trim();
|
|
1332
1958
|
clearHighlights();
|
|
1959
|
+
if (m.ghost) {
|
|
1960
|
+
// the hit lives in a turn the paged window has not loaded: page it in
|
|
1961
|
+
// and land on it (landOnTurn re-applies the highlight there)
|
|
1962
|
+
openTurn(m.turn, m.seq);
|
|
1963
|
+
revealGroupInOutline(m.gi);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1333
1966
|
var r = findRowStrict(m.key);
|
|
1334
1967
|
if (r && q) {
|
|
1335
1968
|
// occurrence index within the target message (consecutive in matches)
|
|
1336
1969
|
var occ = 0;
|
|
1337
1970
|
for (var p = i - 1; p >= 0 && matches[p].key === m.key; p--) occ++;
|
|
1338
|
-
highlightRow(r, q, occ);
|
|
1971
|
+
highlightRow(r, q, occ, fuzzy);
|
|
1339
1972
|
var markEl = r.querySelector(".dqt-current");
|
|
1340
1973
|
var sp = r.closest ? r.closest("[data-conversation-scroll]") : null;
|
|
1341
1974
|
if (sp && markEl) {
|
|
@@ -1352,15 +1985,7 @@ window.__ModuleLoader__.load({
|
|
|
1352
1985
|
} else {
|
|
1353
1986
|
jump(m.key, m.idx);
|
|
1354
1987
|
}
|
|
1355
|
-
|
|
1356
|
-
return Math.max(prev, Math.min(groups.length, groups.length - m.gi));
|
|
1357
|
-
});
|
|
1358
|
-
setTimeout(function () {
|
|
1359
|
-
var el = listRef.current;
|
|
1360
|
-
if (!el) return;
|
|
1361
|
-
var node = el.querySelector('[data-group-idx="' + m.gi + '"]');
|
|
1362
|
-
if (node) node.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1363
|
-
}, 150);
|
|
1988
|
+
revealGroupInOutline(m.gi);
|
|
1364
1989
|
};
|
|
1365
1990
|
|
|
1366
1991
|
// clicking result row `i` = make its FIRST occurrence the current match, so
|
|
@@ -1417,6 +2042,47 @@ window.__ModuleLoader__.load({
|
|
|
1417
2042
|
};
|
|
1418
2043
|
|
|
1419
2044
|
// ---- panel ----
|
|
2045
|
+
// what the module-scope render helpers need from this render
|
|
2046
|
+
var renderExtra = {
|
|
2047
|
+
paths: headingPaths,
|
|
2048
|
+
fuzzy: fuzzy,
|
|
2049
|
+
onOpenTurn: openTurn,
|
|
2050
|
+
hoverStart: hoverStart,
|
|
2051
|
+
hoverEnd: hoverEnd
|
|
2052
|
+
};
|
|
2053
|
+
// Transient banner pinned to the bottom of the panel (the panel is
|
|
2054
|
+
// position:fixed, so this anchors to the list's lower edge without joining
|
|
2055
|
+
// the list's scroll flow). pointerEvents:none keeps it from eating clicks.
|
|
2056
|
+
var toastEl = toast ? react_jsx_runtime.jsx("div", {
|
|
2057
|
+
className: "dqt-toast" + (toast.closing ? " dqt-toast-closing" : ""),
|
|
2058
|
+
style: {
|
|
2059
|
+
position: "absolute",
|
|
2060
|
+
left: 8,
|
|
2061
|
+
right: 8,
|
|
2062
|
+
bottom: 8,
|
|
2063
|
+
display: "flex",
|
|
2064
|
+
justifyContent: "center",
|
|
2065
|
+
pointerEvents: "none",
|
|
2066
|
+
zIndex: 3
|
|
2067
|
+
},
|
|
2068
|
+
children: react_jsx_runtime.jsx("span", {
|
|
2069
|
+
style: {
|
|
2070
|
+
maxWidth: "100%",
|
|
2071
|
+
padding: "3px 10px",
|
|
2072
|
+
borderRadius: 999,
|
|
2073
|
+
cornerShape: "round",
|
|
2074
|
+
fontSize: 11,
|
|
2075
|
+
color: C.text,
|
|
2076
|
+
background: C.panelBg,
|
|
2077
|
+
border: "1px solid " + C.panelBorder,
|
|
2078
|
+
boxShadow: "0 2px 10px rgba(0,0,0,0.22)",
|
|
2079
|
+
whiteSpace: "nowrap",
|
|
2080
|
+
overflow: "hidden",
|
|
2081
|
+
textOverflow: "ellipsis"
|
|
2082
|
+
},
|
|
2083
|
+
children: toast.text
|
|
2084
|
+
})
|
|
2085
|
+
}) : null;
|
|
1420
2086
|
// panel: opening slides in with a slow fade; closing slides quickly to
|
|
1421
2087
|
// the dock edge, clipped by the sidebar line (looks covered, not
|
|
1422
2088
|
// dissolving) and only fades at the very end. No box-shadow: a shadow
|
|
@@ -1441,7 +2107,12 @@ window.__ModuleLoader__.load({
|
|
|
1441
2107
|
}
|
|
1442
2108
|
}
|
|
1443
2109
|
// faded out entirely while another center-column view is active
|
|
1444
|
-
|
|
2110
|
+
// Idle transparency used to be 0.45, which stacked with the outline's own
|
|
2111
|
+
// inactive-group dimming (0.6) into a wall of grey that left the panel barely
|
|
2112
|
+
// readable whenever the pointer was elsewhere. Readability now comes from
|
|
2113
|
+
// CONTRAST on the group being read (tinted row + accent bar, see
|
|
2114
|
+
// renderGroups) instead of from fading everything else away.
|
|
2115
|
+
var panelOpacity = open && chatViewActive ? (hovered ? 1 : 0.72) : 0;
|
|
1445
2116
|
|
|
1446
2117
|
var panelEl = react_jsx_runtime.jsx("div", {
|
|
1447
2118
|
style: {
|
|
@@ -1491,7 +2162,7 @@ window.__ModuleLoader__.load({
|
|
|
1491
2162
|
cursor: "grab",
|
|
1492
2163
|
userSelect: "none"
|
|
1493
2164
|
},
|
|
1494
|
-
title: "
|
|
2165
|
+
title: T("handle.dragY"),
|
|
1495
2166
|
onPointerDown: onHandleDown,
|
|
1496
2167
|
onMouseEnter: function () { handleBright(true); },
|
|
1497
2168
|
onMouseLeave: function () { handleBright(false); },
|
|
@@ -1524,7 +2195,7 @@ window.__ModuleLoader__.load({
|
|
|
1524
2195
|
// three-bar outline mark (long / medium / short) — no title text
|
|
1525
2196
|
react_jsx_runtime.jsx("div", {
|
|
1526
2197
|
style: { display: "flex", alignItems: "center", color: C.muted, flex: "none" },
|
|
1527
|
-
title: "
|
|
2198
|
+
title: T("panel.title"),
|
|
1528
2199
|
children: react_jsx_runtime.jsx("svg", {
|
|
1529
2200
|
width: 18,
|
|
1530
2201
|
height: 14,
|
|
@@ -1540,33 +2211,61 @@ window.__ModuleLoader__.load({
|
|
|
1540
2211
|
react_jsx_runtime.jsx("div", {
|
|
1541
2212
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
1542
2213
|
children: [
|
|
1543
|
-
//
|
|
1544
|
-
//
|
|
2214
|
+
// heading-level filter: a round icon button (directly left
|
|
2215
|
+
// of the search button) that pops down the H1–H6 picker.
|
|
2216
|
+
// Icon: three lines of decreasing width = outline levels.
|
|
2217
|
+
// Hover behaviour matches the magnifier: background + icon
|
|
2218
|
+
// tint change; the resting look never shifts when open.
|
|
1545
2219
|
react_jsx_runtime.jsx("button", {
|
|
1546
|
-
|
|
1547
|
-
|
|
2220
|
+
className: "dqt-levels-btn",
|
|
2221
|
+
onClick: function () { levelsOpen ? closeLevels() : setLevelsOpen(true); },
|
|
2222
|
+
title: T("levels.tip"),
|
|
1548
2223
|
style: {
|
|
1549
|
-
|
|
1550
|
-
|
|
2224
|
+
width: 24,
|
|
2225
|
+
height: 24,
|
|
2226
|
+
padding: 0,
|
|
1551
2227
|
border: "none",
|
|
1552
|
-
borderRadius:
|
|
2228
|
+
borderRadius: "50%",
|
|
1553
2229
|
cornerShape: "round",
|
|
1554
|
-
fontFamily: "inherit",
|
|
1555
|
-
fontSize: 11,
|
|
1556
|
-
lineHeight: "20px",
|
|
1557
2230
|
cursor: "pointer",
|
|
1558
2231
|
flex: "none",
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
2232
|
+
display: "flex",
|
|
2233
|
+
alignItems: "center",
|
|
2234
|
+
justifyContent: "center",
|
|
2235
|
+
// open (or still animating shut) uses the SAME tint as every
|
|
2236
|
+
// other "on" control, so the header reads consistently
|
|
2237
|
+
background: (levelsOpen || levelsClosing) ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
2238
|
+
color: (levelsOpen || levelsClosing) ? C.accent : C.muted,
|
|
2239
|
+
transition: "background 0.15s ease, color 0.15s ease"
|
|
2240
|
+
},
|
|
2241
|
+
onMouseEnter: function (e) {
|
|
2242
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
2243
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
2244
|
+
},
|
|
2245
|
+
onMouseLeave: function (e) {
|
|
2246
|
+
e.currentTarget.style.background = (levelsOpen || levelsClosing) ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
2247
|
+
e.currentTarget.style.color = (levelsOpen || levelsClosing) ? C.accent : C.muted;
|
|
1563
2248
|
},
|
|
1564
|
-
children: "
|
|
2249
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
2250
|
+
width: 14,
|
|
2251
|
+
height: 14,
|
|
2252
|
+
viewBox: "0 0 14 14",
|
|
2253
|
+
fill: "none",
|
|
2254
|
+
stroke: "currentColor",
|
|
2255
|
+
strokeWidth: 1.8,
|
|
2256
|
+
strokeLinecap: "round",
|
|
2257
|
+
style: { display: "block" },
|
|
2258
|
+
children: [
|
|
2259
|
+
react_jsx_runtime.jsx("line", { x1: 1.5, y1: 3, x2: 12.5, y2: 3 }),
|
|
2260
|
+
react_jsx_runtime.jsx("line", { x1: 1.5, y1: 7, x2: 9, y2: 7 }),
|
|
2261
|
+
react_jsx_runtime.jsx("line", { x1: 1.5, y1: 11, x2: 5.5, y2: 11 })
|
|
2262
|
+
]
|
|
2263
|
+
})
|
|
1565
2264
|
}),
|
|
1566
2265
|
// magnifier button (SVG, matches the other buttons' style)
|
|
1567
2266
|
react_jsx_runtime.jsx("button", {
|
|
1568
2267
|
onClick: function () { searchOpen ? closeSearch() : openSearch(); },
|
|
1569
|
-
title: "
|
|
2268
|
+
title: T("search.open"),
|
|
1570
2269
|
style: {
|
|
1571
2270
|
width: 24,
|
|
1572
2271
|
height: 24,
|
|
@@ -1575,9 +2274,9 @@ window.__ModuleLoader__.load({
|
|
|
1575
2274
|
justifyContent: "center",
|
|
1576
2275
|
borderRadius: "50%",
|
|
1577
2276
|
cornerShape: "round",
|
|
1578
|
-
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
2277
|
+
background: searchOpen ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
1579
2278
|
border: "none",
|
|
1580
|
-
color: C.muted,
|
|
2279
|
+
color: searchOpen ? C.accent : C.muted,
|
|
1581
2280
|
cursor: "pointer"
|
|
1582
2281
|
},
|
|
1583
2282
|
onMouseEnter: function (e) {
|
|
@@ -1585,8 +2284,8 @@ window.__ModuleLoader__.load({
|
|
|
1585
2284
|
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
1586
2285
|
},
|
|
1587
2286
|
onMouseLeave: function (e) {
|
|
1588
|
-
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
1589
|
-
e.currentTarget.style.color = C.muted;
|
|
2287
|
+
e.currentTarget.style.background = searchOpen ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
2288
|
+
e.currentTarget.style.color = searchOpen ? C.accent : C.muted;
|
|
1590
2289
|
},
|
|
1591
2290
|
children: react_jsx_runtime.jsx("svg", {
|
|
1592
2291
|
width: 15,
|
|
@@ -1606,11 +2305,11 @@ window.__ModuleLoader__.load({
|
|
|
1606
2305
|
})
|
|
1607
2306
|
}),
|
|
1608
2307
|
// triangle tips toward the side it will move TO: shift slightly up + toward the tip
|
|
1609
|
-
iconBtn(toggleDock, dockRight ? "
|
|
2308
|
+
iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 }),
|
|
1610
2309
|
// close: thick SVG cross, nudged slightly down
|
|
1611
2310
|
react_jsx_runtime.jsx("button", {
|
|
1612
2311
|
onClick: function () { setOpen(false); },
|
|
1613
|
-
title: "
|
|
2312
|
+
title: T("handle.collapse"),
|
|
1614
2313
|
style: {
|
|
1615
2314
|
width: 24,
|
|
1616
2315
|
height: 24,
|
|
@@ -1654,67 +2353,6 @@ window.__ModuleLoader__.load({
|
|
|
1654
2353
|
})
|
|
1655
2354
|
]
|
|
1656
2355
|
}),
|
|
1657
|
-
// level filter row (H1–H6): can be collapsed away with the 层级 button
|
|
1658
|
-
// in the panel header
|
|
1659
|
-
showLevelsRow ? react_jsx_runtime.jsx("div", {
|
|
1660
|
-
style: {
|
|
1661
|
-
flex: "none",
|
|
1662
|
-
display: "flex",
|
|
1663
|
-
alignItems: "center",
|
|
1664
|
-
justifyContent: "space-between",
|
|
1665
|
-
flexWrap: "wrap",
|
|
1666
|
-
rowGap: 2,
|
|
1667
|
-
gap: 6,
|
|
1668
|
-
padding: "3px 10px 3px",
|
|
1669
|
-
borderBottom: crumb ? "none" : "1px solid " + C.panelBorder
|
|
1670
|
-
},
|
|
1671
|
-
children: [
|
|
1672
|
-
react_jsx_runtime.jsx("div", {
|
|
1673
|
-
style: { display: "flex", alignItems: "center", gap: 2, flex: "none" },
|
|
1674
|
-
children: [1, 2, 3, 4, 5, 6].map(function (lv) {
|
|
1675
|
-
var active = !!levelSet[lv];
|
|
1676
|
-
return react_jsx_runtime.jsx("button", {
|
|
1677
|
-
onClick: function () { toggleLevel(lv); },
|
|
1678
|
-
title: active ? "隐藏 H" + lv : "显示 H" + lv,
|
|
1679
|
-
style: {
|
|
1680
|
-
width: 17,
|
|
1681
|
-
height: 17,
|
|
1682
|
-
padding: 0,
|
|
1683
|
-
border: "none",
|
|
1684
|
-
borderRadius: 4,
|
|
1685
|
-
cornerShape: "round",
|
|
1686
|
-
cursor: "pointer",
|
|
1687
|
-
fontSize: 10,
|
|
1688
|
-
lineHeight: "17px",
|
|
1689
|
-
background: active
|
|
1690
|
-
? "rgba(79,140,255,0.22)"
|
|
1691
|
-
: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1692
|
-
color: active ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
1693
|
-
fontWeight: active ? 600 : 400
|
|
1694
|
-
},
|
|
1695
|
-
children: lv
|
|
1696
|
-
}, "lv" + lv);
|
|
1697
|
-
})
|
|
1698
|
-
})
|
|
1699
|
-
]
|
|
1700
|
-
}) : null,
|
|
1701
|
-
// breadcrumb: the heading path you are currently reading
|
|
1702
|
-
crumb ? react_jsx_runtime.jsx("div", {
|
|
1703
|
-
onClick: function () { jump(crumb.key, crumb.idx); },
|
|
1704
|
-
title: crumb.path,
|
|
1705
|
-
style: {
|
|
1706
|
-
flex: "none",
|
|
1707
|
-
padding: "0 10px 4px",
|
|
1708
|
-
borderBottom: "1px solid " + C.panelBorder,
|
|
1709
|
-
fontSize: 10,
|
|
1710
|
-
color: C.muted,
|
|
1711
|
-
whiteSpace: "nowrap",
|
|
1712
|
-
overflow: "hidden",
|
|
1713
|
-
textOverflow: "ellipsis",
|
|
1714
|
-
cursor: "pointer"
|
|
1715
|
-
},
|
|
1716
|
-
children: crumb.path
|
|
1717
|
-
}) : null,
|
|
1718
2356
|
// search input row: outer grid row animates 0fr<->1fr so the row
|
|
1719
2357
|
// collapses/expands to its EXACT natural height (no max-height
|
|
1720
2358
|
// overshoot stutter); the outline below moves smoothly with it
|
|
@@ -1783,23 +2421,23 @@ window.__ModuleLoader__.load({
|
|
|
1783
2421
|
color: "var(--dsw-alias-label-tertiary, rgba(128,128,128,0.7))"
|
|
1784
2422
|
},
|
|
1785
2423
|
children: [
|
|
1786
|
-
"
|
|
2424
|
+
T("search.word"),
|
|
1787
2425
|
react_jsx_runtime.jsx("span", {
|
|
1788
2426
|
style: { position: "relative", display: "inline-block" },
|
|
1789
2427
|
children: [
|
|
1790
2428
|
react_jsx_runtime.jsx("span", {
|
|
1791
2429
|
key: searchScope,
|
|
1792
2430
|
style: { display: "inline-block", animation: "dqt-fade-in 0.25s linear" },
|
|
1793
|
-
children: searchScope === "title" ? "
|
|
2431
|
+
children: searchScope === "title" ? T("search.scope.title") : T("search.scope.full")
|
|
1794
2432
|
}),
|
|
1795
2433
|
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
1796
2434
|
key: "old-" + prevScope,
|
|
1797
2435
|
style: { position: "absolute", left: 0, top: 0, opacity: 0, animation: "dqt-fade-out 0.25s linear forwards" },
|
|
1798
|
-
children: prevScope === "title" ? "
|
|
2436
|
+
children: prevScope === "title" ? T("search.scope.title") : T("search.scope.full")
|
|
1799
2437
|
}) : null
|
|
1800
2438
|
]
|
|
1801
2439
|
}),
|
|
1802
|
-
"
|
|
2440
|
+
T("search.tail")
|
|
1803
2441
|
]
|
|
1804
2442
|
}) : null,
|
|
1805
2443
|
]
|
|
@@ -1828,7 +2466,7 @@ window.__ModuleLoader__.load({
|
|
|
1828
2466
|
// label text cross-fades old->new)
|
|
1829
2467
|
react_jsx_runtime.jsx("button", {
|
|
1830
2468
|
onClick: toggleScope,
|
|
1831
|
-
title: searchScope === "title" ? "
|
|
2469
|
+
title: searchScope === "title" ? T("search.scope.tipTitle") : T("search.scope.tipFull"),
|
|
1832
2470
|
style: {
|
|
1833
2471
|
flex: "none",
|
|
1834
2472
|
width: 34,
|
|
@@ -1840,7 +2478,7 @@ window.__ModuleLoader__.load({
|
|
|
1840
2478
|
cornerShape: "round",
|
|
1841
2479
|
fontSize: 11,
|
|
1842
2480
|
color: searchScope === "full" ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
1843
|
-
background: searchScope === "full" ?
|
|
2481
|
+
background: searchScope === "full" ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1844
2482
|
border: "none",
|
|
1845
2483
|
cursor: "pointer",
|
|
1846
2484
|
whiteSpace: "nowrap",
|
|
@@ -1852,15 +2490,40 @@ window.__ModuleLoader__.load({
|
|
|
1852
2490
|
react_jsx_runtime.jsx("span", {
|
|
1853
2491
|
key: searchScope,
|
|
1854
2492
|
style: { animation: "dqt-fade-in 0.25s linear", display: "block" },
|
|
1855
|
-
children: searchScope === "title" ? "
|
|
2493
|
+
children: searchScope === "title" ? T("search.scope.title") : T("search.scope.full")
|
|
1856
2494
|
}),
|
|
1857
2495
|
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
1858
2496
|
key: "old-" + prevScope,
|
|
1859
2497
|
style: { position: "absolute", opacity: 0, animation: "dqt-fade-out 0.25s linear forwards", display: "block" },
|
|
1860
|
-
children: prevScope === "title" ? "
|
|
2498
|
+
children: prevScope === "title" ? T("search.scope.title") : T("search.scope.full")
|
|
1861
2499
|
}) : null
|
|
1862
2500
|
]
|
|
1863
2501
|
})
|
|
2502
|
+
}),
|
|
2503
|
+
// fuzzy switch: an independent toggle beside the scope pill (its label
|
|
2504
|
+
// is the same either way, so only the colour carries the state)
|
|
2505
|
+
react_jsx_runtime.jsx("button", {
|
|
2506
|
+
onClick: toggleFuzzy,
|
|
2507
|
+
title: fuzzy ? T("search.fuzzy.tipOn") : T("search.fuzzy.tipOff"),
|
|
2508
|
+
"data-fuzzy": fuzzy ? "on" : "off",
|
|
2509
|
+
style: {
|
|
2510
|
+
flex: "none",
|
|
2511
|
+
width: 34,
|
|
2512
|
+
height: 34,
|
|
2513
|
+
display: "flex",
|
|
2514
|
+
alignItems: "center",
|
|
2515
|
+
justifyContent: "center",
|
|
2516
|
+
borderRadius: "50%",
|
|
2517
|
+
cornerShape: "round",
|
|
2518
|
+
fontSize: 11,
|
|
2519
|
+
color: fuzzy ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
2520
|
+
background: fuzzy ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
2521
|
+
border: "none",
|
|
2522
|
+
cursor: "pointer",
|
|
2523
|
+
whiteSpace: "nowrap",
|
|
2524
|
+
transition: "background 0.25s ease, color 0.25s ease"
|
|
2525
|
+
},
|
|
2526
|
+
children: T("search.fuzzy")
|
|
1864
2527
|
})
|
|
1865
2528
|
]
|
|
1866
2529
|
})
|
|
@@ -1887,10 +2550,32 @@ window.__ModuleLoader__.load({
|
|
|
1887
2550
|
children: react_jsx_runtime.jsx("div", {
|
|
1888
2551
|
style: { direction: "ltr" },
|
|
1889
2552
|
children: query.trim()
|
|
1890
|
-
?
|
|
1891
|
-
|
|
2553
|
+
? [
|
|
2554
|
+
react_jsx_runtime.jsx("div", {
|
|
2555
|
+
key: "results",
|
|
2556
|
+
children: renderResults(resultRows, query.trim(), C, goToRow, activeResultRow, renderExtra)
|
|
2557
|
+
}),
|
|
2558
|
+
// older history is one upward scroll away — say so until the
|
|
2559
|
+
// reader actually scrolls up (then the hint has done its job)
|
|
2560
|
+
hint ? react_jsx_runtime.jsx("div", {
|
|
2561
|
+
key: "hint",
|
|
2562
|
+
style: {
|
|
2563
|
+
display: "flex",
|
|
2564
|
+
alignItems: "center",
|
|
2565
|
+
justifyContent: "center",
|
|
2566
|
+
padding: "10px 6px 4px",
|
|
2567
|
+
fontSize: 11,
|
|
2568
|
+
color: C.muted,
|
|
2569
|
+
opacity: 0.85,
|
|
2570
|
+
textAlign: "center"
|
|
2571
|
+
},
|
|
2572
|
+
children: hint === "oldest" ? T("search.hintOldest") : T("search.hintMore")
|
|
2573
|
+
}) : null
|
|
2574
|
+
]
|
|
2575
|
+
: renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup, renderExtra)
|
|
1892
2576
|
})
|
|
1893
2577
|
}),
|
|
2578
|
+
toastEl,
|
|
1894
2579
|
// resize handles (right edge: width, bottom edge: height)
|
|
1895
2580
|
react_jsx_runtime.jsx("div", {
|
|
1896
2581
|
style: {
|
|
@@ -1904,7 +2589,7 @@ window.__ModuleLoader__.load({
|
|
|
1904
2589
|
zIndex: 2
|
|
1905
2590
|
},
|
|
1906
2591
|
onPointerDown: onResizeWDown,
|
|
1907
|
-
title: "
|
|
2592
|
+
title: T("resize.w")
|
|
1908
2593
|
}),
|
|
1909
2594
|
react_jsx_runtime.jsx("div", {
|
|
1910
2595
|
style: {
|
|
@@ -1918,7 +2603,7 @@ window.__ModuleLoader__.load({
|
|
|
1918
2603
|
zIndex: 2
|
|
1919
2604
|
},
|
|
1920
2605
|
onPointerDown: onResizeHDown,
|
|
1921
|
-
title: "
|
|
2606
|
+
title: T("resize.h")
|
|
1922
2607
|
}),
|
|
1923
2608
|
// corner handle: resize width AND height at once
|
|
1924
2609
|
react_jsx_runtime.jsx("div", {
|
|
@@ -1933,8 +2618,59 @@ window.__ModuleLoader__.load({
|
|
|
1933
2618
|
zIndex: 3
|
|
1934
2619
|
},
|
|
1935
2620
|
onPointerDown: onResizeCornerDown,
|
|
1936
|
-
title: "
|
|
1937
|
-
})
|
|
2621
|
+
title: T("resize.wh")
|
|
2622
|
+
}),
|
|
2623
|
+
// heading-level picker: pops down from the round header button,
|
|
2624
|
+
// right-aligned, with an outer shadow so it reads as a floating layer.
|
|
2625
|
+
// Open/close animate from the button's center: the animation lives in
|
|
2626
|
+
// the injected stylesheet (classes, not inline animation) so React
|
|
2627
|
+
// re-renders never restart or cancel it — an inline `animation` set on
|
|
2628
|
+
// every render made the entrance invisible and the exit never play.
|
|
2629
|
+
(levelsOpen || levelsClosing) ? react_jsx_runtime.jsx("div", {
|
|
2630
|
+
className: "dqt-levels-pop" + (levelsClosing ? " dqt-levels-pop-closing" : ""),
|
|
2631
|
+
style: {
|
|
2632
|
+
position: "absolute",
|
|
2633
|
+
top: 40,
|
|
2634
|
+
right: 8,
|
|
2635
|
+
zIndex: 30,
|
|
2636
|
+
display: "flex",
|
|
2637
|
+
alignItems: "center",
|
|
2638
|
+
flexWrap: "wrap",
|
|
2639
|
+
gap: 4,
|
|
2640
|
+
padding: 8,
|
|
2641
|
+
background: C.panelBg,
|
|
2642
|
+
border: "1px solid " + C.panelBorder,
|
|
2643
|
+
borderRadius: 10,
|
|
2644
|
+
cornerShape: "round",
|
|
2645
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.28), 0 2px 6px rgba(0, 0, 0, 0.18)",
|
|
2646
|
+
maxWidth: "calc(100% - 16px)",
|
|
2647
|
+
transformOrigin: "calc(50% + 12px) top"
|
|
2648
|
+
},
|
|
2649
|
+
children: [1, 2, 3, 4, 5, 6].map(function (lv) {
|
|
2650
|
+
var active = !!levelSet[lv];
|
|
2651
|
+
return react_jsx_runtime.jsx("button", {
|
|
2652
|
+
onClick: function () { toggleLevel(lv); },
|
|
2653
|
+
title: active ? T("levels.hide") + lv : T("levels.show") + lv,
|
|
2654
|
+
style: {
|
|
2655
|
+
height: 22,
|
|
2656
|
+
padding: "0 7px",
|
|
2657
|
+
border: "none",
|
|
2658
|
+
borderRadius: 6,
|
|
2659
|
+
cornerShape: "round",
|
|
2660
|
+
cursor: "pointer",
|
|
2661
|
+
fontSize: 11,
|
|
2662
|
+
lineHeight: "22px",
|
|
2663
|
+
fontFamily: "inherit",
|
|
2664
|
+
background: active
|
|
2665
|
+
? C.chip
|
|
2666
|
+
: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
2667
|
+
color: active ? C.accent : C.muted,
|
|
2668
|
+
fontWeight: active ? 600 : 400
|
|
2669
|
+
},
|
|
2670
|
+
children: "H" + lv
|
|
2671
|
+
}, "lv" + lv);
|
|
2672
|
+
})
|
|
2673
|
+
}) : null
|
|
1938
2674
|
]
|
|
1939
2675
|
});
|
|
1940
2676
|
|
|
@@ -1958,7 +2694,7 @@ window.__ModuleLoader__.load({
|
|
|
1958
2694
|
? { right: viewport ? viewport.right + 52 : 60 } // clear of the milestone rail
|
|
1959
2695
|
: { left: viewport ? viewport.left : 0 })
|
|
1960
2696
|
},
|
|
1961
|
-
title: "
|
|
2697
|
+
title: T("handle.expand"),
|
|
1962
2698
|
onClick: function () { setOpen(true); },
|
|
1963
2699
|
onMouseEnter: function () { if (edgeRef.current) edgeRef.current.style.opacity = "1"; },
|
|
1964
2700
|
onMouseLeave: function () { if (edgeRef.current) edgeRef.current.style.opacity = "0.5"; },
|
|
@@ -1993,29 +2729,119 @@ window.__ModuleLoader__.load({
|
|
|
1993
2729
|
})
|
|
1994
2730
|
}) : null;
|
|
1995
2731
|
|
|
2732
|
+
// ---- hover preview card ------------------------------------------------
|
|
2733
|
+
// Rendered INSIDE the body portal (position: fixed) so the outline's own
|
|
2734
|
+
// scroll container can never clip it, and pointer-events: none so it can
|
|
2735
|
+
// never steal the hover that opened it.
|
|
2736
|
+
var hoverEl = null;
|
|
2737
|
+
if (hoverCard && open) {
|
|
2738
|
+
var cardW = 264;
|
|
2739
|
+
var cardTop = Math.max(8, Math.min(hoverCard.top - 4, window.innerHeight - 210));
|
|
2740
|
+
var cardLeft = dockRight
|
|
2741
|
+
? Math.max(8, hoverCard.left - cardW - 10)
|
|
2742
|
+
: Math.min(window.innerWidth - cardW - 8, hoverCard.right + 10);
|
|
2743
|
+
var cardBody = hoverCard.preview || hoverCard.sub || "";
|
|
2744
|
+
var cardChildren = [
|
|
2745
|
+
react_jsx_runtime.jsx("div", {
|
|
2746
|
+
style: { fontSize: 12, fontWeight: 600, color: C.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
2747
|
+
children: hoverCard.title
|
|
2748
|
+
}, "hc-t")
|
|
2749
|
+
];
|
|
2750
|
+
if (hoverCard.meta) {
|
|
2751
|
+
cardChildren.push(react_jsx_runtime.jsx("div", {
|
|
2752
|
+
style: { fontSize: 10, color: C.muted, opacity: 0.8, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
2753
|
+
children: hoverCard.meta
|
|
2754
|
+
}, "hc-m"));
|
|
2755
|
+
}
|
|
2756
|
+
cardChildren.push(react_jsx_runtime.jsx("div", {
|
|
2757
|
+
style: { fontSize: 11, lineHeight: "16px", color: C.text, opacity: 0.9, marginTop: 4, maxHeight: 116, overflow: "hidden" },
|
|
2758
|
+
children: cardBody || (hoverCard.ghost ? T("turn.loadTip") : "")
|
|
2759
|
+
}, "hc-b"));
|
|
2760
|
+
if (hoverCard.ghost) {
|
|
2761
|
+
cardChildren.push(react_jsx_runtime.jsx("div", {
|
|
2762
|
+
style: { fontSize: 10, color: C.muted, opacity: 0.7, marginTop: 4 },
|
|
2763
|
+
children: T("preview.truncated")
|
|
2764
|
+
}, "hc-n"));
|
|
2765
|
+
}
|
|
2766
|
+
hoverEl = react_jsx_runtime.jsx("div", {
|
|
2767
|
+
className: "dqt-hover" + (hoverClosing ? " dqt-hover-closing" : ""),
|
|
2768
|
+
style: {
|
|
2769
|
+
position: "fixed",
|
|
2770
|
+
top: cardTop,
|
|
2771
|
+
left: cardLeft,
|
|
2772
|
+
width: cardW,
|
|
2773
|
+
padding: "8px 10px",
|
|
2774
|
+
borderRadius: 8,
|
|
2775
|
+
cornerShape: "round",
|
|
2776
|
+
background: C.panelBg,
|
|
2777
|
+
color: C.text,
|
|
2778
|
+
border: "1px solid " + C.panelBorder,
|
|
2779
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.28)",
|
|
2780
|
+
zIndex: Z_BASE + 1,
|
|
2781
|
+
pointerEvents: "none"
|
|
2782
|
+
},
|
|
2783
|
+
children: cardChildren
|
|
2784
|
+
}, "hover-card");
|
|
2785
|
+
}
|
|
2786
|
+
|
|
1996
2787
|
return react_dom.createPortal(
|
|
1997
|
-
react_jsx_runtime.jsx(ErrorBoundary, { children: [panelEl, edgeEl] }),
|
|
2788
|
+
react_jsx_runtime.jsx(ErrorBoundary, { children: [panelEl, edgeEl, hoverEl] }),
|
|
1998
2789
|
document.body
|
|
1999
2790
|
);
|
|
2000
2791
|
}
|
|
2001
2792
|
|
|
2002
|
-
function renderItem(n, depth, jump, C, uid) {
|
|
2793
|
+
function renderItem(n, depth, jump, C, uid, extra) {
|
|
2003
2794
|
var hasChildren = !!(n.children && n.children.length);
|
|
2004
|
-
var
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2795
|
+
var path = extra && extra.paths ? extra.paths[headingId(n)] : null;
|
|
2796
|
+
var hoverInfo = {
|
|
2797
|
+
title: n.title,
|
|
2798
|
+
sub: n.sub || "",
|
|
2799
|
+
preview: n.preview || "",
|
|
2800
|
+
meta: [n.time, path ? path.path : ""].filter(Boolean).join(" · "),
|
|
2801
|
+
ghost: false
|
|
2802
|
+
};
|
|
2803
|
+
var rows = [
|
|
2804
|
+
react_jsx_runtime.jsx("div", {
|
|
2805
|
+
style: { display: "flex", alignItems: "center", gap: 2, minWidth: 0 },
|
|
2806
|
+
children: react_jsx_runtime.jsx("span", {
|
|
2807
|
+
style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" },
|
|
2808
|
+
children: n.title
|
|
2809
|
+
})
|
|
2810
|
+
}, uid + "-l")
|
|
2811
|
+
];
|
|
2812
|
+
// Subtitle = the section's first sentence. This is what makes two headings
|
|
2813
|
+
// with the SAME text distinguishable without hovering ("which one is this?").
|
|
2814
|
+
if (n.sub) {
|
|
2815
|
+
rows.push(react_jsx_runtime.jsx("div", {
|
|
2816
|
+
style: {
|
|
2817
|
+
fontSize: 10,
|
|
2818
|
+
lineHeight: "13px",
|
|
2819
|
+
color: C.muted,
|
|
2820
|
+
opacity: 0.75,
|
|
2821
|
+
whiteSpace: "nowrap",
|
|
2822
|
+
overflow: "hidden",
|
|
2823
|
+
textOverflow: "ellipsis"
|
|
2824
|
+
},
|
|
2825
|
+
children: n.sub
|
|
2826
|
+
}, uid + "-s"));
|
|
2827
|
+
}
|
|
2009
2828
|
return react_jsx_runtime.jsx(
|
|
2010
2829
|
"div",
|
|
2011
2830
|
{
|
|
2012
2831
|
onClick: function () { jump(n.key, n.idx); },
|
|
2832
|
+
onMouseEnter: function (e) {
|
|
2833
|
+
e.currentTarget.style.background = C.hover;
|
|
2834
|
+
if (extra && extra.hoverStart) extra.hoverStart(hoverInfo, e.currentTarget);
|
|
2835
|
+
},
|
|
2836
|
+
onMouseLeave: function (e) {
|
|
2837
|
+
e.currentTarget.style.background = "transparent";
|
|
2838
|
+
if (extra && extra.hoverEnd) extra.hoverEnd();
|
|
2839
|
+
},
|
|
2013
2840
|
"data-jump-key": n.key,
|
|
2014
2841
|
"data-jump-idx": n.idx !== undefined ? String(n.idx) : "0",
|
|
2015
2842
|
style: {
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
gap: 2,
|
|
2843
|
+
// no fixed height: a row grows by one line when it has a subtitle
|
|
2844
|
+
display: "block",
|
|
2019
2845
|
padding: "2px 6px",
|
|
2020
2846
|
paddingLeft: (hasChildren ? 2 : 6) + (n.level - 1) * 12,
|
|
2021
2847
|
margin: "1px 0",
|
|
@@ -2025,26 +2851,23 @@ window.__ModuleLoader__.load({
|
|
|
2025
2851
|
color: n.level <= 2 ? C.text : C.muted,
|
|
2026
2852
|
fontWeight: n.level <= 2 ? 600 : 400,
|
|
2027
2853
|
lineHeight: "18px",
|
|
2028
|
-
height: 22,
|
|
2029
2854
|
whiteSpace: "nowrap",
|
|
2030
2855
|
overflow: "hidden"
|
|
2031
2856
|
},
|
|
2032
|
-
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
2033
|
-
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
2034
2857
|
title: n.title,
|
|
2035
|
-
children:
|
|
2858
|
+
children: rows
|
|
2036
2859
|
},
|
|
2037
2860
|
uid + "-" + n.level + "-" + (n.key || "")
|
|
2038
2861
|
);
|
|
2039
2862
|
}
|
|
2040
2863
|
|
|
2041
|
-
function renderNodes(nodes, depth, jump, C, uid) {
|
|
2864
|
+
function renderNodes(nodes, depth, jump, C, uid, extra) {
|
|
2042
2865
|
var out = [];
|
|
2043
2866
|
for (var i = 0; i < nodes.length; i++) {
|
|
2044
2867
|
var n = nodes[i];
|
|
2045
|
-
out.push(renderItem(n, depth, jump, C, uid + "-" + i));
|
|
2868
|
+
out.push(renderItem(n, depth, jump, C, uid + "-" + i, extra));
|
|
2046
2869
|
if (n.children && n.children.length) {
|
|
2047
|
-
out.push(react_jsx_runtime.jsx("div", { children: renderNodes(n.children, depth + 1, jump, C, uid + "-" + i) }, uid + "-c-" + i));
|
|
2870
|
+
out.push(react_jsx_runtime.jsx("div", { children: renderNodes(n.children, depth + 1, jump, C, uid + "-" + i, extra) }, uid + "-c-" + i));
|
|
2048
2871
|
}
|
|
2049
2872
|
}
|
|
2050
2873
|
return out;
|
|
@@ -2054,8 +2877,8 @@ window.__ModuleLoader__.load({
|
|
|
2054
2877
|
// While a query is present the list shows every matched heading/message
|
|
2055
2878
|
// (grouped per message, with an occurrence count) instead of the outline, so
|
|
2056
2879
|
// search is "see them all, then jump" rather than stepping blindly.
|
|
2057
|
-
function hitSpans(text, q) {
|
|
2058
|
-
var parts = highlightParts(text, q);
|
|
2880
|
+
function hitSpans(text, q, fuzzy) {
|
|
2881
|
+
var parts = highlightParts(text, q, fuzzy);
|
|
2059
2882
|
return parts.map(function (p, i) {
|
|
2060
2883
|
return p.hit
|
|
2061
2884
|
? react_jsx_runtime.jsx("span", {
|
|
@@ -2066,8 +2889,10 @@ window.__ModuleLoader__.load({
|
|
|
2066
2889
|
});
|
|
2067
2890
|
}
|
|
2068
2891
|
|
|
2069
|
-
function renderResultRow(r, i, q, C, onRow, isActive) {
|
|
2892
|
+
function renderResultRow(r, i, q, C, onRow, isActive, extra) {
|
|
2893
|
+
var fuzzy = !!(extra && extra.fuzzy);
|
|
2070
2894
|
var meta = [];
|
|
2895
|
+
if (r.ghost) meta.push(T("turn.unloaded"));
|
|
2071
2896
|
if (r.time) meta.push(r.time);
|
|
2072
2897
|
if (r.path) meta.push(r.path);
|
|
2073
2898
|
if (r.count > 1) meta.push("×" + r.count);
|
|
@@ -2081,7 +2906,7 @@ window.__ModuleLoader__.load({
|
|
|
2081
2906
|
overflow: "hidden",
|
|
2082
2907
|
textOverflow: "ellipsis"
|
|
2083
2908
|
},
|
|
2084
|
-
children: hitSpans(r.title, q)
|
|
2909
|
+
children: hitSpans(r.title, q, fuzzy)
|
|
2085
2910
|
}, "t" + i)
|
|
2086
2911
|
];
|
|
2087
2912
|
if (r.snippet) {
|
|
@@ -2094,7 +2919,7 @@ window.__ModuleLoader__.load({
|
|
|
2094
2919
|
overflow: "hidden",
|
|
2095
2920
|
textOverflow: "ellipsis"
|
|
2096
2921
|
},
|
|
2097
|
-
children: hitSpans(r.snippet, q)
|
|
2922
|
+
children: hitSpans(r.snippet, q, fuzzy)
|
|
2098
2923
|
}, "s" + i));
|
|
2099
2924
|
}
|
|
2100
2925
|
if (meta.length) {
|
|
@@ -2108,9 +2933,23 @@ window.__ModuleLoader__.load({
|
|
|
2108
2933
|
// keyword in the conversation and scrolls there, exactly like Enter
|
|
2109
2934
|
// stepping — a row that only scrolled (no highlight) was the old bug
|
|
2110
2935
|
onClick: function () { onRow(i); },
|
|
2936
|
+
onMouseEnter: function (e) {
|
|
2937
|
+
e.currentTarget.style.background = C.hover;
|
|
2938
|
+
if (extra && extra.hoverStart) {
|
|
2939
|
+
extra.hoverStart({
|
|
2940
|
+
title: r.title,
|
|
2941
|
+
sub: r.snippet || "",
|
|
2942
|
+
preview: r.snippet || "",
|
|
2943
|
+
meta: meta.join(" · "),
|
|
2944
|
+
ghost: !!r.ghost
|
|
2945
|
+
}, e.currentTarget);
|
|
2946
|
+
}
|
|
2947
|
+
},
|
|
2948
|
+
onMouseLeave: function (e) {
|
|
2949
|
+
e.currentTarget.style.background = isActive ? C.chip : "transparent";
|
|
2950
|
+
if (extra && extra.hoverEnd) extra.hoverEnd();
|
|
2951
|
+
},
|
|
2111
2952
|
"data-result-idx": i,
|
|
2112
|
-
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
2113
|
-
onMouseLeave: function (e) { e.currentTarget.style.background = isActive ? "rgba(79,140,255,0.16)" : "transparent"; },
|
|
2114
2953
|
title: r.path || r.title,
|
|
2115
2954
|
style: {
|
|
2116
2955
|
padding: "4px 6px",
|
|
@@ -2118,23 +2957,23 @@ window.__ModuleLoader__.load({
|
|
|
2118
2957
|
borderRadius: 6,
|
|
2119
2958
|
cursor: "pointer",
|
|
2120
2959
|
minWidth: 0,
|
|
2121
|
-
background: isActive ?
|
|
2960
|
+
background: isActive ? C.chip : "transparent",
|
|
2122
2961
|
transition: "background 0.15s ease"
|
|
2123
2962
|
},
|
|
2124
2963
|
children: children
|
|
2125
2964
|
}, "res-" + i);
|
|
2126
2965
|
}
|
|
2127
2966
|
|
|
2128
|
-
function renderResults(rows, q, C, onRow, activeRow) {
|
|
2967
|
+
function renderResults(rows, q, C, onRow, activeRow, extra) {
|
|
2129
2968
|
if (!rows || rows.length === 0) {
|
|
2130
2969
|
return react_jsx_runtime.jsx("div", {
|
|
2131
2970
|
style: { padding: "12px 8px", fontSize: 12, color: C.muted, textAlign: "center" },
|
|
2132
|
-
children: "
|
|
2133
|
-
});
|
|
2971
|
+
children: T("search.empty")
|
|
2972
|
+
}, "empty");
|
|
2134
2973
|
}
|
|
2135
2974
|
var out = [];
|
|
2136
2975
|
for (var i = 0; i < rows.length; i++) {
|
|
2137
|
-
out.push(renderResultRow(rows[i], i, q, C, onRow, i === activeRow));
|
|
2976
|
+
out.push(renderResultRow(rows[i], i, q, C, onRow, i === activeRow, extra));
|
|
2138
2977
|
}
|
|
2139
2978
|
return out;
|
|
2140
2979
|
}
|
|
@@ -2145,9 +2984,8 @@ window.__ModuleLoader__.load({
|
|
|
2145
2984
|
// Implemented as a function (not an inline closure in a loop) so each
|
|
2146
2985
|
// header captures its own (g, gi) — the var-in-loop closure bug would
|
|
2147
2986
|
// otherwise make every header jump to the last group.
|
|
2148
|
-
function renderGroupHeader(g, gi, jump, C) {
|
|
2987
|
+
function renderGroupHeader(g, gi, jump, C, isActive, headingless) {
|
|
2149
2988
|
var replyKey = (g.msgs && g.msgs.length > 0) ? g.msgs[0].key : "";
|
|
2150
|
-
var targetKey = replyKey || g.userKey;
|
|
2151
2989
|
var jumpToTurn = function (e) {
|
|
2152
2990
|
e.stopPropagation();
|
|
2153
2991
|
// prefer the model's reply; fall back to the user message, then to the
|
|
@@ -2158,30 +2996,52 @@ window.__ModuleLoader__.load({
|
|
|
2158
2996
|
};
|
|
2159
2997
|
return react_jsx_runtime.jsx("div", {
|
|
2160
2998
|
style: {
|
|
2161
|
-
padding: "1px 4px 2px",
|
|
2162
|
-
height: 18,
|
|
2999
|
+
padding: headingless ? "3px 4px" : "1px 4px 2px",
|
|
3000
|
+
height: headingless ? undefined : 18,
|
|
2163
3001
|
display: "flex",
|
|
2164
3002
|
alignItems: "center",
|
|
2165
3003
|
minWidth: 0,
|
|
2166
3004
|
// sticky: while you scroll the outline, the header of the group you are
|
|
2167
|
-
// inside stays pinned at the top
|
|
2168
|
-
// the
|
|
3005
|
+
// inside stays pinned at the top. It must be opaque to cover the rows
|
|
3006
|
+
// scrolling underneath, so its base colour is the panel background in both
|
|
3007
|
+
// states — the "current group" tint rides on its own layer inside, which
|
|
3008
|
+
// can FADE (a background-image gradient cannot be transitioned).
|
|
2169
3009
|
position: "sticky",
|
|
2170
3010
|
top: 0,
|
|
2171
3011
|
zIndex: 2,
|
|
2172
3012
|
background: C.panelBg
|
|
2173
3013
|
},
|
|
2174
|
-
children:
|
|
3014
|
+
children: [
|
|
3015
|
+
react_jsx_runtime.jsx("div", {
|
|
3016
|
+
style: {
|
|
3017
|
+
position: "absolute",
|
|
3018
|
+
left: 0,
|
|
3019
|
+
right: 0,
|
|
3020
|
+
top: 0,
|
|
3021
|
+
bottom: 0,
|
|
3022
|
+
background: C.groupTint,
|
|
3023
|
+
opacity: isActive ? 1 : 0,
|
|
3024
|
+
transition: "opacity 0.18s ease",
|
|
3025
|
+
pointerEvents: "none"
|
|
3026
|
+
}
|
|
3027
|
+
}, "g-h-tint-" + gi),
|
|
3028
|
+
react_jsx_runtime.jsx("span", {
|
|
2175
3029
|
onClick: jumpToTurn,
|
|
2176
|
-
title: replyKey ? "
|
|
3030
|
+
title: replyKey ? T("turn.jumpReply") : T("turn.jumpTurn"),
|
|
2177
3031
|
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
2178
3032
|
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
2179
3033
|
style: {
|
|
2180
|
-
|
|
2181
|
-
|
|
3034
|
+
// A turn with no headings has ONLY this row, so it carries the whole
|
|
3035
|
+
// entry: readable size, weight and colour. No extra pill background —
|
|
3036
|
+
// the "current group" tint belongs to the group, so headed and
|
|
3037
|
+
// heading-less turns look the same when they are being read.
|
|
3038
|
+
fontSize: headingless ? 12 : 11,
|
|
3039
|
+
fontWeight: headingless ? 600 : 400,
|
|
3040
|
+
color: headingless ? C.text : C.muted,
|
|
2182
3041
|
cursor: "pointer",
|
|
2183
|
-
padding: "1px 5px",
|
|
3042
|
+
padding: headingless ? "2px 6px" : "1px 5px",
|
|
2184
3043
|
borderRadius: 4,
|
|
3044
|
+
background: "transparent",
|
|
2185
3045
|
transition: "background 0.15s ease",
|
|
2186
3046
|
display: "inline-flex",
|
|
2187
3047
|
alignItems: "center",
|
|
@@ -2193,18 +3053,81 @@ window.__ModuleLoader__.load({
|
|
|
2193
3053
|
children: [
|
|
2194
3054
|
react_jsx_runtime.jsx("span", { style: { fontWeight: 600, flex: "none" }, children: g.time || " " }),
|
|
2195
3055
|
g.userText ? react_jsx_runtime.jsx("span", {
|
|
2196
|
-
style: {
|
|
3056
|
+
style: {
|
|
3057
|
+
fontWeight: headingless ? 500 : 400,
|
|
3058
|
+
opacity: headingless ? 0.95 : 0.75,
|
|
3059
|
+
overflow: "hidden",
|
|
3060
|
+
textOverflow: "ellipsis",
|
|
3061
|
+
whiteSpace: "nowrap",
|
|
3062
|
+
minWidth: 0
|
|
3063
|
+
},
|
|
2197
3064
|
children: g.userText
|
|
2198
3065
|
}) : null
|
|
2199
3066
|
]
|
|
2200
3067
|
})
|
|
3068
|
+
]
|
|
2201
3069
|
}, "g-h-" + gi);
|
|
2202
3070
|
}
|
|
2203
3071
|
|
|
3072
|
+
// A turn the paged event window has not loaded yet: the host outline still
|
|
3073
|
+
// names it (number + bounded previews), so the outline covers the whole
|
|
3074
|
+
// session. Clicking pages that turn in through the session's jump loader.
|
|
3075
|
+
function renderGhostGroup(g, gi, C, extra) {
|
|
3076
|
+
extra = extra || {};
|
|
3077
|
+
var children = [
|
|
3078
|
+
react_jsx_runtime.jsx("div", {
|
|
3079
|
+
style: { display: "flex", alignItems: "center", gap: 5, minWidth: 0 },
|
|
3080
|
+
children: [
|
|
3081
|
+
react_jsx_runtime.jsx("span", {
|
|
3082
|
+
style: { fontSize: 10, fontWeight: 600, flex: "none", color: C.muted, opacity: 0.8, border: "1px solid " + C.panelBorder, borderRadius: 4, padding: "0 4px" },
|
|
3083
|
+
children: T("turn.unloaded")
|
|
3084
|
+
}),
|
|
3085
|
+
react_jsx_runtime.jsx("span", {
|
|
3086
|
+
style: { fontSize: 11, color: C.muted, fontWeight: 400, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 },
|
|
3087
|
+
children: g.userText || "#" + g.turn
|
|
3088
|
+
})
|
|
3089
|
+
]
|
|
3090
|
+
})
|
|
3091
|
+
];
|
|
3092
|
+
if (g.preview) {
|
|
3093
|
+
children.push(react_jsx_runtime.jsx("div", {
|
|
3094
|
+
style: { fontSize: 10, lineHeight: "13px", color: C.muted, opacity: 0.65, marginTop: 1, overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" },
|
|
3095
|
+
children: g.preview
|
|
3096
|
+
}));
|
|
3097
|
+
}
|
|
3098
|
+
return react_jsx_runtime.jsx("div", {
|
|
3099
|
+
onClick: function () { extra.onOpenTurn(g.turn, g.seq); },
|
|
3100
|
+
onMouseEnter: function (e) {
|
|
3101
|
+
e.currentTarget.style.background = C.hover;
|
|
3102
|
+
e.currentTarget.style.opacity = "1";
|
|
3103
|
+
extra.hoverStart({ title: g.userText || "#" + g.turn, sub: "", preview: g.preview || "", meta: T("turn.unloaded"), ghost: true }, e.currentTarget);
|
|
3104
|
+
},
|
|
3105
|
+
onMouseLeave: function (e) {
|
|
3106
|
+
e.currentTarget.style.background = "transparent";
|
|
3107
|
+
e.currentTarget.style.opacity = "0.85";
|
|
3108
|
+
extra.hoverEnd();
|
|
3109
|
+
},
|
|
3110
|
+
"data-group-idx": gi,
|
|
3111
|
+
"data-ghost-turn": g.turn,
|
|
3112
|
+
title: T("turn.loadTip"),
|
|
3113
|
+
style: {
|
|
3114
|
+
padding: "3px 6px",
|
|
3115
|
+
margin: "1px 0",
|
|
3116
|
+
borderRadius: 6,
|
|
3117
|
+
cursor: "pointer",
|
|
3118
|
+
minWidth: 0,
|
|
3119
|
+
opacity: 0.85,
|
|
3120
|
+
transition: "background 0.15s ease, opacity 0.15s ease"
|
|
3121
|
+
},
|
|
3122
|
+
children: children
|
|
3123
|
+
}, "g-ghost-" + gi);
|
|
3124
|
+
}
|
|
3125
|
+
|
|
2204
3126
|
// render each conversation turn as its own block: solid divider + time header
|
|
2205
3127
|
// offset = global group index of the first rendered group (stable React keys)
|
|
2206
3128
|
// activeIdx = array of groups being read (full opacity); others are dimmed
|
|
2207
|
-
function renderGroups(groups, trees, jump, C, offset, activeIdx) {
|
|
3129
|
+
function renderGroups(groups, trees, jump, C, offset, activeIdx, extra) {
|
|
3130
|
+
extra = extra || {};
|
|
2208
3131
|
var base = offset || 0;
|
|
2209
3132
|
var activeSet = {};
|
|
2210
3133
|
if (Array.isArray(activeIdx)) {
|
|
@@ -2214,24 +3137,55 @@ window.__ModuleLoader__.load({
|
|
|
2214
3137
|
for (var i = 0; i < groups.length; i++) {
|
|
2215
3138
|
var g = groups[i];
|
|
2216
3139
|
var gi = base + i;
|
|
2217
|
-
var
|
|
2218
|
-
|
|
3140
|
+
var isActive = !!activeSet[gi];
|
|
3141
|
+
var rows = g.ghost ? [] : trees[i];
|
|
3142
|
+
// The divider between two groups stays OUTSIDE the highlight box: inside it
|
|
3143
|
+
// would draw a rule straight across the tinted block.
|
|
3144
|
+
out.push(react_jsx_runtime.jsx("div", {
|
|
2219
3145
|
style: { borderTop: "1px solid " + C.panelBorder, margin: "7px 2px 3px", height: 0 }
|
|
2220
3146
|
}, "g-sep-" + gi));
|
|
2221
|
-
items
|
|
2222
|
-
|
|
2223
|
-
|
|
3147
|
+
var items = [];
|
|
3148
|
+
if (g.ghost) {
|
|
3149
|
+
items.push(renderGhostGroup(g, gi, C, extra));
|
|
3150
|
+
} else {
|
|
3151
|
+
items.push(renderGroupHeader(g, gi, jump, C, isActive, !rows || rows.length === 0));
|
|
3152
|
+
items.push(renderNodes(rows, 0, jump, C, "g" + gi, extra));
|
|
3153
|
+
}
|
|
3154
|
+
// Inactive groups used to fade to 0.6, which — multiplied by the panel's own
|
|
3155
|
+
// idle opacity — turned the whole list into grey. 0.85 keeps every row
|
|
3156
|
+
// readable, and "the group being read" is marked by a CLOSED tinted box
|
|
3157
|
+
// (fill + outline + accent bar) instead, which stays visible on top of any
|
|
3158
|
+
// panel transparency and no longer looks cut off on the right.
|
|
3159
|
+
var dim = !isActive && !g.ghost;
|
|
2224
3160
|
out.push(react_jsx_runtime.jsx("div", {
|
|
2225
3161
|
"data-group-idx": gi,
|
|
2226
|
-
|
|
3162
|
+
"data-active-group": isActive ? "on" : "off",
|
|
3163
|
+
style: {
|
|
3164
|
+
opacity: dim ? 0.85 : 1,
|
|
3165
|
+
// the box fades in AND out: colour transitions only, and the left accent
|
|
3166
|
+
// keeps its 3px width in both states so activating a group never reflows
|
|
3167
|
+
// (a width change would shift every row by 2px)
|
|
3168
|
+
transition: "opacity 0.15s ease, background-color 0.18s ease, border-color 0.18s ease",
|
|
3169
|
+
backgroundColor: isActive ? C.groupTint : "transparent",
|
|
3170
|
+
borderTop: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
3171
|
+
borderRight: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
3172
|
+
borderBottom: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
3173
|
+
borderLeft: "3px solid " + (isActive ? C.groupEdge : "transparent"),
|
|
3174
|
+
borderRadius: 6,
|
|
3175
|
+
margin: "2px -5px",
|
|
3176
|
+
paddingLeft: 2
|
|
3177
|
+
},
|
|
2227
3178
|
children: items
|
|
2228
3179
|
}, "g-" + gi));
|
|
2229
3180
|
}
|
|
2230
3181
|
return out;
|
|
2231
3182
|
}
|
|
2232
3183
|
|
|
3184
|
+
// Host-facing locale namespace: the slot declares `locale: "dsh-quick-toc"`,
|
|
3185
|
+
// which is what hands the panel its `t` prop. The panel's own visible strings
|
|
3186
|
+
// live in DICTS and are kept in sync with this table.
|
|
2233
3187
|
var zh = {
|
|
2234
|
-
"panel.title": "
|
|
3188
|
+
"panel.title": DICTS.zh["panel.title"]
|
|
2235
3189
|
};
|
|
2236
3190
|
var en = {
|
|
2237
3191
|
"panel.title": "Conversation Outline"
|
|
@@ -2243,6 +3197,29 @@ window.__ModuleLoader__.load({
|
|
|
2243
3197
|
ctx.effect(function () {
|
|
2244
3198
|
return ctx.locale.register("dsh-quick-toc", { zh: zh, en: en });
|
|
2245
3199
|
}, "dsh-quick-toc: dictionaries");
|
|
3200
|
+
|
|
3201
|
+
// Bridge to the host session face, used to open a turn whose events the
|
|
3202
|
+
// paged window has not loaded: ctx.sessions.binding(id).session.loadThrough(seq)
|
|
3203
|
+
// is the host's documented "page history backwards until the window covers
|
|
3204
|
+
// seq" jump loader.
|
|
3205
|
+
//
|
|
3206
|
+
// The client plugin facade offers two ways in: a direct `ctx.sessions`
|
|
3207
|
+
// property read (which demands `inject: ["sessions"]` on the returned plugin
|
|
3208
|
+
// object, and the runtime then PARKS the whole package whenever that provider
|
|
3209
|
+
// is absent) and `ctx.get(name)`, the declaration-free lookup. This uses the
|
|
3210
|
+
// lookup — deliberately optional, so a host without the session controller
|
|
3211
|
+
// still gets a working outline, and callers report the missing loader
|
|
3212
|
+
// instead of the panel disappearing.
|
|
3213
|
+
var host = {
|
|
3214
|
+
sessions: function () {
|
|
3215
|
+
try {
|
|
3216
|
+
return typeof ctx.get === "function" ? (ctx.get("sessions") || null) : null;
|
|
3217
|
+
} catch (e) {
|
|
3218
|
+
return null;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
};
|
|
3222
|
+
|
|
2246
3223
|
// Recent DSH (0.1.5-rc.1): session-scoped hooks (useChat/useSession/sessionId) only arrive
|
|
2247
3224
|
// inside a declared session slot. Register the panel into the session-scoped
|
|
2248
3225
|
// conversation.input.overlay (list/additive) so it receives useChat; the panel
|
|
@@ -2254,7 +3231,10 @@ window.__ModuleLoader__.load({
|
|
|
2254
3231
|
id: "quick-toc",
|
|
2255
3232
|
order: 90,
|
|
2256
3233
|
locale: "dsh-quick-toc"
|
|
2257
|
-
},
|
|
3234
|
+
}, function OutlinePanelWithHost(props) {
|
|
3235
|
+
// the panel needs the host bridge plus the session id it was registered for
|
|
3236
|
+
return OutlinePanel(Object.assign({}, props, { tocHost: host }));
|
|
3237
|
+
});
|
|
2258
3238
|
});
|
|
2259
3239
|
}
|
|
2260
3240
|
|
|
@@ -2269,6 +3249,21 @@ window.__ModuleLoader__.load({
|
|
|
2269
3249
|
var s = document.createElement("style");
|
|
2270
3250
|
s.id = "dsh-quick-toc-css";
|
|
2271
3251
|
s.textContent = "@keyframes dqt-spin{to{transform:rotate(360deg)}}@keyframes dqt-fade-in{from{opacity:0}to{opacity:1}}@keyframes dqt-fade-out{from{opacity:1}to{opacity:0}}" +
|
|
3252
|
+
"@keyframes dqt-pop-in{from{opacity:0;transform:scale(0.7)}to{opacity:1;transform:scale(1)}}@keyframes dqt-pop-out{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(0.7)}}" +
|
|
3253
|
+
// the level picker's enter/exit animations live here as classes so React
|
|
3254
|
+
// re-renders (which rewrite inline styles) cannot restart or cancel them
|
|
3255
|
+
".dqt-levels-pop{animation:dqt-pop-in 0.22s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
|
|
3256
|
+
// hover preview card: fades in place, and fades OUT when the pointer
|
|
3257
|
+
// leaves (both as stylesheet classes so re-renders cannot cancel them)
|
|
3258
|
+
".dqt-hover{animation:dqt-fade-in 0.12s linear both}" +
|
|
3259
|
+
".dqt-hover-closing{animation:dqt-fade-out 0.16s cubic-bezier(0.2, 0.9, 0.3, 1) both}" +
|
|
3260
|
+
// transient edge banner over the bottom of the outline list
|
|
3261
|
+
"@keyframes dqt-toast-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}" +
|
|
3262
|
+
"@keyframes dqt-toast-out{from{opacity:1;transform:none}to{opacity:0;transform:none}}" +
|
|
3263
|
+
".dqt-toast{animation:dqt-toast-in 0.2s cubic-bezier(0.22, 0.9, 0.3, 1) both}" +
|
|
3264
|
+
".dqt-toast-closing{animation:dqt-toast-out 0.42s linear both}" +
|
|
3265
|
+
// exit: starts moving immediately (fast attack), so the dismiss feels snappy
|
|
3266
|
+
".dqt-levels-pop-closing{animation:dqt-pop-out 0.14s cubic-bezier(0.2, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
|
|
2272
3267
|
// Hide the native scrollbar entirely (no arrow buttons, no grey bar) —
|
|
2273
3268
|
// custom webkit scrollbar styling was not reliably suppressing the
|
|
2274
3269
|
// default arrows, so a hidden native bar + the panel's inner shadow gives
|