dsh-quick-toc 0.8.2 → 0.9.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 +20 -0
- package/CHANGELOG.md +20 -0
- package/README.en.md +15 -11
- package/README.md +14 -10
- package/lib/client.js +1073 -369
- package/lib/index.js +5 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -60,6 +60,10 @@ window.__ModuleLoader__.load({
|
|
|
60
60
|
// panel's collapse takes 0.28s, and the host can re-measure the conversation area at
|
|
61
61
|
// any point while it runs, so the freeze outlives the motion by a frame or two.
|
|
62
62
|
var COLLAPSE_HOLD_MS = 420;
|
|
63
|
+
// how long the long-list virtualization stays held back after a surface
|
|
64
|
+
// (docked panel / curtain) starts animating: the open slide runs 0.6s and the
|
|
65
|
+
// curtain's drop 0.46s, so 780ms covers both with margin
|
|
66
|
+
var VIRT_HOLD_MS = 780;
|
|
63
67
|
// The row-end control ("jump to the end of this section") lives in the last 24px
|
|
64
68
|
// of a row (a 20px button inset 4px). Every box a row can paint — the hover
|
|
65
69
|
// highlight, a group header's hover label, the right-aligned context text — stops
|
|
@@ -101,23 +105,22 @@ window.__ModuleLoader__.load({
|
|
|
101
105
|
"a11y.results": "共 {n} 条结果",
|
|
102
106
|
"a11y.loading": "正在检索",
|
|
103
107
|
"a11y.crossEmpty": "没有结果",
|
|
104
|
-
"sheet.open": "全屏大纲(从顶部展开一层幕布)",
|
|
105
108
|
"sheet.openHandle": "展开幕布:全宽大纲从顶部落下",
|
|
106
109
|
"sheet.close": "收起幕布",
|
|
107
110
|
"sheet.noDrag": "幕布铺满对话区,不能拖动尺寸或位置",
|
|
108
|
-
"
|
|
111
|
+
"settings.open": "打开插件设置",
|
|
112
|
+
"settings.missing": "当前部署没有给本插件提供设置页面。",
|
|
109
113
|
"levels.show": "显示 H",
|
|
110
114
|
"levels.hide": "隐藏 H",
|
|
111
|
-
"levels.offInQuestions": "只看提问时没有标题行,层级筛选不生效",
|
|
112
|
-
"view.questions.tipOn": "当前:只看提问(每轮只显示时间与你的提问)。点击回到完整大纲",
|
|
113
|
-
"view.questions.tipOff": "当前:完整大纲。点击只看提问(每轮只显示时间与你的提问)",
|
|
114
115
|
"search.open": "搜索标题",
|
|
115
116
|
"search.word": "搜索",
|
|
116
117
|
"search.tail": ",回车定位…",
|
|
117
118
|
"search.scope.title": "标题",
|
|
119
|
+
"search.scope.prompts": "提问",
|
|
118
120
|
"search.scope.full": "全文",
|
|
119
121
|
"search.scope.cross": "会话",
|
|
120
|
-
"search.scope.tipTitle": "
|
|
122
|
+
"search.scope.tipTitle": "当前:仅搜索标题。点击切换为只看提问",
|
|
123
|
+
"search.scope.tipPrompts": "当前:只看提问(搜索只覆盖你的提问,搜索框留空即为提问列表)。点击切换为全文搜索",
|
|
121
124
|
"search.scope.tipFull": "当前:全文搜索。点击切换为跨会话检索",
|
|
122
125
|
"search.scope.tipCross": "当前:跨会话检索(用宿主的全文索引搜所有会话的消息正文)。点击回到仅搜索标题",
|
|
123
126
|
"search.cross.loading": "检索中…",
|
|
@@ -135,6 +138,8 @@ window.__ModuleLoader__.load({
|
|
|
135
138
|
"search.fuzzy.tipOn": "模糊匹配已开启:允许关键字中间夹少量其他文字,命中更多",
|
|
136
139
|
"search.fuzzy.tipOff": "模糊匹配已关闭:只匹配连续的文字。点击开启",
|
|
137
140
|
"search.empty": "没有匹配",
|
|
141
|
+
"search.history": "最近搜索",
|
|
142
|
+
"search.history.clear": "清空",
|
|
138
143
|
"search.hintMore": "向上滚动可加载更早的消息",
|
|
139
144
|
"search.hintOldest": "已经是最早的消息",
|
|
140
145
|
"hint.autoOff": "上面还有更早的历史(自动加载已关闭,点「未加载」的条目才会加载)",
|
|
@@ -171,6 +176,9 @@ window.__ModuleLoader__.load({
|
|
|
171
176
|
"settings.dock.left": "左侧",
|
|
172
177
|
"settings.dock.right": "右侧",
|
|
173
178
|
"settings.levels": "显示的标题层级",
|
|
179
|
+
"settings.density": "大纲密度",
|
|
180
|
+
"settings.density.comfy": "标准",
|
|
181
|
+
"settings.density.compact": "紧凑",
|
|
174
182
|
"settings.zoom": "面板缩放",
|
|
175
183
|
"settings.zoom.tip": "文字、图标与按钮按比例缩放,面板本身的尺寸不变",
|
|
176
184
|
"settings.sheetZoom": "幕布缩放",
|
|
@@ -202,23 +210,22 @@ window.__ModuleLoader__.load({
|
|
|
202
210
|
"a11y.results": "{n} result(s)",
|
|
203
211
|
"a11y.loading": "Searching",
|
|
204
212
|
"a11y.crossEmpty": "No results",
|
|
205
|
-
"sheet.open": "Full-width outline (a sheet that drops from the top)",
|
|
206
213
|
"sheet.openHandle": "Open the curtain: a full-width outline drops from the top",
|
|
207
214
|
"sheet.close": "Close the sheet",
|
|
208
215
|
"sheet.noDrag": "The sheet spans the conversation area, so it cannot be dragged or resized",
|
|
209
|
-
"
|
|
216
|
+
"settings.open": "Open the plugin settings",
|
|
217
|
+
"settings.missing": "This deployment exposes no settings screen for the plugin.",
|
|
210
218
|
"levels.show": "Show H",
|
|
211
219
|
"levels.hide": "Hide H",
|
|
212
|
-
"levels.offInQuestions": "Questions-only view has no heading rows, so the level filter does not apply",
|
|
213
|
-
"view.questions.tipOn": "Questions only: each turn shows just its time and your prompt. Click for the full outline",
|
|
214
|
-
"view.questions.tipOff": "Full outline. Click to show each turn's time and your prompt only",
|
|
215
220
|
"search.open": "Search headings",
|
|
216
221
|
"search.word": "Search",
|
|
217
222
|
"search.tail": ", Enter to jump…",
|
|
218
223
|
"search.scope.title": "Title",
|
|
224
|
+
"search.scope.prompts": "Prompts",
|
|
219
225
|
"search.scope.full": "Full text",
|
|
220
226
|
"search.scope.cross": "Session",
|
|
221
|
-
"search.scope.tipTitle": "Titles only. Click for
|
|
227
|
+
"search.scope.tipTitle": "Titles only. Click for prompts only",
|
|
228
|
+
"search.scope.tipPrompts": "Prompts only: the search covers your own questions, and an empty box lists them. Click for full-text search",
|
|
222
229
|
"search.scope.tipFull": "Full text. Click for cross-session search",
|
|
223
230
|
"search.scope.tipCross": "Cross-session search (the host's full-text index over every session's message text). Click to go back to titles only",
|
|
224
231
|
"search.cross.loading": "Searching…",
|
|
@@ -236,6 +243,8 @@ window.__ModuleLoader__.load({
|
|
|
236
243
|
"search.fuzzy.tipOn": "Fuzzy matching is on: a few other characters may sit between the keywords, so more matches hit",
|
|
237
244
|
"search.fuzzy.tipOff": "Fuzzy matching is off: only contiguous text matches. Click to turn it on",
|
|
238
245
|
"search.empty": "No matches",
|
|
246
|
+
"search.history": "Recent searches",
|
|
247
|
+
"search.history.clear": "Clear",
|
|
239
248
|
"search.hintMore": "Scroll up to load earlier messages",
|
|
240
249
|
"search.hintOldest": "This is the earliest message",
|
|
241
250
|
"hint.autoOff": "There is older history above (auto-loading is off — click an unloaded row to load it)",
|
|
@@ -274,6 +283,9 @@ window.__ModuleLoader__.load({
|
|
|
274
283
|
"settings.dock.left": "Left",
|
|
275
284
|
"settings.dock.right": "Right",
|
|
276
285
|
"settings.levels": "Heading levels shown",
|
|
286
|
+
"settings.density": "Outline density",
|
|
287
|
+
"settings.density.comfy": "Comfy",
|
|
288
|
+
"settings.density.compact": "Compact",
|
|
277
289
|
"settings.zoom": "Panel scale",
|
|
278
290
|
"settings.zoom.tip": "Scales the text, icons and buttons; the panel's own size is unchanged",
|
|
279
291
|
"settings.sheetZoom": "Curtain scale",
|
|
@@ -358,12 +370,12 @@ window.__ModuleLoader__.load({
|
|
|
358
370
|
// purpose — they describe THIS screen, not the account.
|
|
359
371
|
var NAMESPACE = "dsh-quick-toc";
|
|
360
372
|
// Fields backed by the Host settings document; everything else is localStorage.
|
|
361
|
-
var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, zoom: 1, sheetZoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1, autoLoad: 1 };
|
|
373
|
+
var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, density: 1, zoom: 1, sheetZoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1, autoLoad: 1 };
|
|
362
374
|
// The schema defaults (mirrored here for the card's per-field reset and for the
|
|
363
375
|
// one-time import of a 0.5.x install's stored values).
|
|
364
376
|
var DEFAULTS = {
|
|
365
377
|
dock: "left", y: 0, w: 0, h: 0,
|
|
366
|
-
levels: [1, 2, 3, 4, 5, 6],
|
|
378
|
+
levels: [1, 2, 3, 4, 5, 6], density: "comfy",
|
|
367
379
|
zoom: 1, sheetZoom: 1, handle: 0.5,
|
|
368
380
|
fuzzy: false, hover: true, lang: "auto", debug: false,
|
|
369
381
|
remember: true, autoLoad: true
|
|
@@ -374,6 +386,7 @@ window.__ModuleLoader__.load({
|
|
|
374
386
|
w: "dsh-quick-toc.panelW.v1",
|
|
375
387
|
h: "dsh-quick-toc.panelH.v1",
|
|
376
388
|
levels: "dsh-quick-toc.levels.v1",
|
|
389
|
+
density: "dsh-quick-toc.density.v1",
|
|
377
390
|
zoom: "dsh-quick-toc.zoom.v1",
|
|
378
391
|
sheetZoom: "dsh-quick-toc.sheetZoom.v1",
|
|
379
392
|
handle: "dsh-quick-toc.handle.v1",
|
|
@@ -448,6 +461,47 @@ window.__ModuleLoader__.load({
|
|
|
448
461
|
localStorage.setItem(READ_POS_KEY, JSON.stringify(map));
|
|
449
462
|
} catch (e) {}
|
|
450
463
|
};
|
|
464
|
+
// ---- recent search history (browser-local, like the reading position) --
|
|
465
|
+
// Most-recent-first [{q, scope}]: filled when a search is committed (Enter in
|
|
466
|
+
// the box, a result click), offered back as one-tap chips while the box is
|
|
467
|
+
// empty. Plain data in, plain data out — the component owns the state.
|
|
468
|
+
var SEARCH_HIST_KEY = "dsh-quick-toc.searchHist.v1";
|
|
469
|
+
var SEARCH_HIST_MAX = 10;
|
|
470
|
+
var searchHistLoad = function () {
|
|
471
|
+
try {
|
|
472
|
+
var raw = localStorage.getItem(SEARCH_HIST_KEY);
|
|
473
|
+
var parsed = raw ? JSON.parse(raw) : null;
|
|
474
|
+
if (!Array.isArray(parsed)) return [];
|
|
475
|
+
var out = [];
|
|
476
|
+
for (var i = 0; i < parsed.length && out.length < SEARCH_HIST_MAX; i++) {
|
|
477
|
+
var e = parsed[i];
|
|
478
|
+
if (e && typeof e.q === "string" && e.q.trim() !== "") {
|
|
479
|
+
out.push({
|
|
480
|
+
q: e.q.slice(0, 80),
|
|
481
|
+
scope: e.scope === "full" || e.scope === "cross" ? e.scope : "title"
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
} catch (e) { return []; }
|
|
487
|
+
};
|
|
488
|
+
var searchHistRemember = function (list, q, scope) {
|
|
489
|
+
var text = typeof q === "string" ? q.trim().slice(0, 80) : "";
|
|
490
|
+
if (text === "") return Array.isArray(list) ? list : [];
|
|
491
|
+
var s = scope === "full" || scope === "cross" ? scope : "title";
|
|
492
|
+
var out = [{ q: text, scope: s }];
|
|
493
|
+
if (Array.isArray(list)) {
|
|
494
|
+
for (var i = 0; i < list.length && out.length < SEARCH_HIST_MAX; i++) {
|
|
495
|
+
if (list[i] && list[i].q !== text) out.push(list[i]);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
try { localStorage.setItem(SEARCH_HIST_KEY, JSON.stringify(out)); } catch (e) {}
|
|
499
|
+
return out;
|
|
500
|
+
};
|
|
501
|
+
var searchHistClear = function () {
|
|
502
|
+
try { localStorage.removeItem(SEARCH_HIST_KEY); } catch (e) {}
|
|
503
|
+
return [];
|
|
504
|
+
};
|
|
451
505
|
var prefsStore = (function () {
|
|
452
506
|
var hasOwn = function (obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); };
|
|
453
507
|
var read = function (key, legacy) {
|
|
@@ -507,6 +561,7 @@ window.__ModuleLoader__.load({
|
|
|
507
561
|
w: wRaw !== null && isFinite(Number(wRaw)) && Number(wRaw) >= 120 ? Number(wRaw) : 0,
|
|
508
562
|
h: hRaw !== null && isFinite(Number(hRaw)) && Number(hRaw) >= 60 ? Number(hRaw) : 0,
|
|
509
563
|
levels: parseLevels(rawLevels) || LEVELS_ALL.slice(),
|
|
564
|
+
density: read(PREF_KEYS.density) === "compact" ? "compact" : "comfy",
|
|
510
565
|
zoom: clampRatio(zoomRaw, 1),
|
|
511
566
|
sheetZoom: clampRatio(sheetZoomRaw, 1),
|
|
512
567
|
handle: clampRatio(handleRaw, 0.5),
|
|
@@ -528,6 +583,7 @@ window.__ModuleLoader__.load({
|
|
|
528
583
|
w: function (v) { return String(v); },
|
|
529
584
|
h: function (v) { return String(v); },
|
|
530
585
|
levels: function (v) { return JSON.stringify(v); },
|
|
586
|
+
density: function (v) { return v === "compact" ? "compact" : "comfy"; },
|
|
531
587
|
zoom: function (v) { return String(v); },
|
|
532
588
|
sheetZoom: function (v) { return String(v); },
|
|
533
589
|
handle: function (v) { return String(v); },
|
|
@@ -562,6 +618,7 @@ window.__ModuleLoader__.load({
|
|
|
562
618
|
dock: v.dock === "right" ? "right" : "left",
|
|
563
619
|
lang: v.lang === "zh" || v.lang === "en" ? v.lang : "auto",
|
|
564
620
|
levels: levels || LEVELS_ALL.slice(),
|
|
621
|
+
density: v.density === "compact" ? "compact" : "comfy",
|
|
565
622
|
zoom: isFinite(zoomNum) ? Math.min(2, Math.max(0.5, Math.round(zoomNum * 100) / 100)) : 1,
|
|
566
623
|
sheetZoom: isFinite(sheetZoomNum) ? Math.min(2, Math.max(0.5, Math.round(sheetZoomNum * 100) / 100)) : 1,
|
|
567
624
|
handle: isFinite(handleNum) ? Math.min(1, Math.max(0, Math.round(handleNum * 100) / 100)) : 0.5,
|
|
@@ -578,7 +635,7 @@ window.__ModuleLoader__.load({
|
|
|
578
635
|
if (!over) return local;
|
|
579
636
|
return {
|
|
580
637
|
dock: over.dock, y: local.y, w: local.w, h: local.h,
|
|
581
|
-
levels: over.levels, zoom: over.zoom, sheetZoom: over.sheetZoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
|
|
638
|
+
levels: over.levels, density: over.density, zoom: over.zoom, sheetZoom: over.sheetZoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
|
|
582
639
|
lang: over.lang, remember: over.remember, debug: over.debug, autoLoad: over.autoLoad
|
|
583
640
|
};
|
|
584
641
|
};
|
|
@@ -859,12 +916,21 @@ window.__ModuleLoader__.load({
|
|
|
859
916
|
return extractReplyText(node);
|
|
860
917
|
}
|
|
861
918
|
|
|
862
|
-
// first non-empty line of a message, truncated for the outline header
|
|
919
|
+
// first non-empty line of a message, truncated for the outline header.
|
|
920
|
+
// Scanning for the line instead of splitting the whole text matters: this is
|
|
921
|
+
// called once per match inside the search loops, and a full split per call
|
|
922
|
+
// made typing in the search box quadratic on long sessions.
|
|
863
923
|
function previewText(text, max) {
|
|
864
924
|
if (!text) return "";
|
|
865
|
-
var
|
|
866
|
-
|
|
867
|
-
|
|
925
|
+
var s = String(text);
|
|
926
|
+
var from = 0;
|
|
927
|
+
for (;;) {
|
|
928
|
+
var nl = s.indexOf("\n", from);
|
|
929
|
+
var line = (nl === -1 ? s.slice(from) : s.slice(from, nl)).trim();
|
|
930
|
+
if (line) return line.length > max ? line.slice(0, max) + "…" : line;
|
|
931
|
+
if (nl === -1) return "";
|
|
932
|
+
from = nl + 1;
|
|
933
|
+
}
|
|
868
934
|
}
|
|
869
935
|
|
|
870
936
|
// ---------- normalized matching (fold, plus an optional fuzzy pass) ----------
|
|
@@ -965,7 +1031,103 @@ window.__ModuleLoader__.load({
|
|
|
965
1031
|
function countOccurrences(text, q, fuzzy) {
|
|
966
1032
|
var fq = q && q.folded !== undefined ? q.folded : foldQuery(q);
|
|
967
1033
|
if (!text || !fq) return 0;
|
|
968
|
-
return
|
|
1034
|
+
return countFolded(foldedText(text), fq, !!fuzzy);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// ---------- cached folding for the search scan ----------
|
|
1038
|
+
// Search re-reads the SAME message texts on every keystroke — twice (the
|
|
1039
|
+
// match list and the result rows fold them independently) — so the folded
|
|
1040
|
+
// form is kept per text. Counting never needs the per-character index map,
|
|
1041
|
+
// so the cache stores the plain folded string plus, lazily, the flattened
|
|
1042
|
+
// one-line form the snippets are cut from.
|
|
1043
|
+
var searchFoldCache = new Map();
|
|
1044
|
+
// the fold itself (no cache): whitespace runs collapse to one space, and
|
|
1045
|
+
// full-width forms and case fold 1:1. Char-code tests instead of a regex per
|
|
1046
|
+
// character: this runs over every loaded message whenever a search starts.
|
|
1047
|
+
function foldString(src) {
|
|
1048
|
+
var out = "";
|
|
1049
|
+
var prevSpace = false;
|
|
1050
|
+
for (var i = 0; i < src.length; i++) {
|
|
1051
|
+
var code = src.charCodeAt(i);
|
|
1052
|
+
// whitespace of any kind (case folding never changes it)
|
|
1053
|
+
if (code === 32 || (code >= 9 && code <= 13) || code === 160 || code === 5760 ||
|
|
1054
|
+
(code >= 8192 && code <= 8202) || code === 8232 || code === 8233 || code === 8239 ||
|
|
1055
|
+
code === 8287 || code === 12288 || code === 65279) {
|
|
1056
|
+
if (prevSpace) continue; // collapse a whitespace run to one space
|
|
1057
|
+
prevSpace = true;
|
|
1058
|
+
out += " ";
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
prevSpace = false;
|
|
1062
|
+
if (code >= 0xFF01 && code <= 0xFF5E) {
|
|
1063
|
+
// full-width ASCII to half-width, then case-fold the result
|
|
1064
|
+
var half = code - 0xFEE0;
|
|
1065
|
+
out += half >= 65 && half <= 90 ? String.fromCharCode(half + 32) : String.fromCharCode(half);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if (code >= 65 && code <= 90) { out += String.fromCharCode(code + 32); continue; }
|
|
1069
|
+
if (code >= 97 && code <= 122 || code < 128) { out += src.charAt(i); continue; }
|
|
1070
|
+
// cased scripts outside ASCII (Greek, Cyrillic, …) fold via toLowerCase
|
|
1071
|
+
out += src.charAt(i).toLowerCase();
|
|
1072
|
+
}
|
|
1073
|
+
return out;
|
|
1074
|
+
}
|
|
1075
|
+
function foldWork(text) {
|
|
1076
|
+
var src = String(text === undefined || text === null ? "" : text);
|
|
1077
|
+
var w = searchFoldCache.get(src);
|
|
1078
|
+
if (w !== undefined) return w;
|
|
1079
|
+
w = { folded: foldString(src), flat: null, flatFolded: null };
|
|
1080
|
+
if (searchFoldCache.size > 1200) searchFoldCache.clear();
|
|
1081
|
+
searchFoldCache.set(src, w);
|
|
1082
|
+
return w;
|
|
1083
|
+
}
|
|
1084
|
+
function foldedText(text) {
|
|
1085
|
+
return foldWork(text).folded;
|
|
1086
|
+
}
|
|
1087
|
+
// the one-line form snippets are sliced from (whitespace collapsed). Its fold
|
|
1088
|
+
// lives on the same entry — a second cache key per message would evict twice
|
|
1089
|
+
// as fast and make every keystroke re-fold the session again.
|
|
1090
|
+
function flatWork(text) {
|
|
1091
|
+
var w = foldWork(text);
|
|
1092
|
+
if (w.flat === null) {
|
|
1093
|
+
w.flat = String(text === undefined || text === null ? "" : text).replace(/\s+/g, " ").trim();
|
|
1094
|
+
w.flatFolded = foldString(w.flat);
|
|
1095
|
+
}
|
|
1096
|
+
return w;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Count only: the match scan without hit objects or ranges. Same walk as
|
|
1100
|
+
// findMatches, so the numbers agree with the stepping list.
|
|
1101
|
+
function countFolded(folded, fq, fuzzy) {
|
|
1102
|
+
if (!folded || !fq) return 0;
|
|
1103
|
+
if (!fuzzy) {
|
|
1104
|
+
var n = 0;
|
|
1105
|
+
var at = 0;
|
|
1106
|
+
while ((at = folded.indexOf(fq, at)) !== -1) {
|
|
1107
|
+
n++;
|
|
1108
|
+
at += fq.length;
|
|
1109
|
+
}
|
|
1110
|
+
return n;
|
|
1111
|
+
}
|
|
1112
|
+
var span = fuzzySpan(fq);
|
|
1113
|
+
var from = 0;
|
|
1114
|
+
var count = 0;
|
|
1115
|
+
for (;;) {
|
|
1116
|
+
var start = -1;
|
|
1117
|
+
var pos = from;
|
|
1118
|
+
var ok = true;
|
|
1119
|
+
for (var i = 0; i < fq.length; i++) {
|
|
1120
|
+
var hit = folded.indexOf(fq.charAt(i), pos);
|
|
1121
|
+
if (hit === -1 || (start !== -1 && hit - start > span)) { ok = false; break; }
|
|
1122
|
+
if (start === -1) start = hit;
|
|
1123
|
+
pos = hit + 1;
|
|
1124
|
+
}
|
|
1125
|
+
if (!ok) break;
|
|
1126
|
+
count++;
|
|
1127
|
+
from = pos;
|
|
1128
|
+
if (from >= folded.length) break;
|
|
1129
|
+
}
|
|
1130
|
+
return count;
|
|
969
1131
|
}
|
|
970
1132
|
|
|
971
1133
|
// ---------- section bodies: subtitle + hover preview ----------
|
|
@@ -1132,17 +1294,408 @@ window.__ModuleLoader__.load({
|
|
|
1132
1294
|
}
|
|
1133
1295
|
|
|
1134
1296
|
// A one-line window around the first hit of `q` in `text` (for search results).
|
|
1297
|
+
// Cuts from the cached flattened form: folding it is index-stable (the
|
|
1298
|
+
// whitespace is already collapsed), so the folded hit position slices it
|
|
1299
|
+
// directly — no per-call re-fold of the whole message.
|
|
1135
1300
|
function snippetAround(text, q, span, fuzzy) {
|
|
1136
1301
|
if (!text) return "";
|
|
1137
|
-
var
|
|
1302
|
+
var w = flatWork(text);
|
|
1138
1303
|
var fq = foldQuery(q);
|
|
1139
|
-
|
|
1140
|
-
var
|
|
1141
|
-
if (
|
|
1142
|
-
var at = folded.map[hits[0].ranges[0][0]];
|
|
1304
|
+
if (!fq) return previewText(w.flat, span * 2);
|
|
1305
|
+
var at = firstHitIndex(w.flatFolded, fq, !!fuzzy);
|
|
1306
|
+
if (at < 0) return previewText(w.flat, span * 2);
|
|
1143
1307
|
var start = Math.max(0, at - span);
|
|
1144
|
-
var end = Math.min(flat.length, at + span * 2);
|
|
1145
|
-
return (start > 0 ? "…" : "") + flat.slice(start, end) + (end < flat.length ? "…" : "");
|
|
1308
|
+
var end = Math.min(w.flat.length, at + span * 2);
|
|
1309
|
+
return (start > 0 ? "…" : "") + w.flat.slice(start, end) + (end < w.flat.length ? "…" : "");
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// folded-space index of the first hit's first character (-1 when there is none)
|
|
1313
|
+
function firstHitIndex(folded, fq, fuzzy) {
|
|
1314
|
+
if (!folded || !fq) return -1;
|
|
1315
|
+
if (!fuzzy) return folded.indexOf(fq);
|
|
1316
|
+
var span = fuzzySpan(fq);
|
|
1317
|
+
var start = -1;
|
|
1318
|
+
var at = 0;
|
|
1319
|
+
for (var i = 0; i < fq.length; i++) {
|
|
1320
|
+
var hit = folded.indexOf(fq.charAt(i), at);
|
|
1321
|
+
if (hit === -1 || (start !== -1 && hit - start > span)) return -1;
|
|
1322
|
+
if (start === -1) start = hit;
|
|
1323
|
+
at = hit + 1;
|
|
1324
|
+
}
|
|
1325
|
+
return start;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// ---------- reaching this plugin's own settings screen ----------
|
|
1329
|
+
// The host's own navigation first (see `openPluginSettingsPage`), then the DOM
|
|
1330
|
+
// chain below, and nothing silent on the way: a click that cannot land warns.
|
|
1331
|
+
//
|
|
1332
|
+
// Measured on 0.2.0-rc.2: the Plugins page publishes every entry as
|
|
1333
|
+
// `data-plugin-item="<id>"` — this plugin's id is `quick-toc` — and renders the
|
|
1334
|
+
// card's title as a button, which is the host's own open gesture. There is no
|
|
1335
|
+
// `data-section` anywhere in this host line, so the navigation-cell route below
|
|
1336
|
+
// is a legacy fallback, not the way in.
|
|
1337
|
+
var SETTINGS_NAV_ITEM_ATTR = "data-section";
|
|
1338
|
+
var SETTINGS_NAV_ITEM_VALUE = "plugins";
|
|
1339
|
+
var SETTINGS_NAV_CURRENT_ATTR = "aria-current";
|
|
1340
|
+
var SETTINGS_LAUNCHER_HASPOPUP = "dialog";
|
|
1341
|
+
var PLUGIN_ITEM_ATTR = "data-plugin-item";
|
|
1342
|
+
var PLUGIN_ITEM_VALUE = "quick-toc";
|
|
1343
|
+
// Labels are compared for EQUALITY, never as substrings: a sidebar session
|
|
1344
|
+
// titled "quick-toc 插件开发" is not this plugin's entry, and treating it as one
|
|
1345
|
+
// is exactly the bug that sent this button to the wrong place.
|
|
1346
|
+
var PLUGIN_TEXTS = ["对话大纲", "quick toc", "quick-toc", "dsh-quick-toc", "conversation outline"];
|
|
1347
|
+
var PLUGIN_CONFIG_TEXTS = ["插件配置", "plugin configuration", "plugin config"];
|
|
1348
|
+
var PLUGIN_CARD_CANDIDATES = "[data-plugin-item], button, a, [role=button], [role=tab], [role=menuitem], [role=link]";
|
|
1349
|
+
var PLUGIN_TAB_CANDIDATES = "button, a, [role=tab], [role=button]";
|
|
1350
|
+
var SETTINGS_NAV_TEXTS = ["插件", "Plugins", "Plugin", "プラグイン", "플러그인"];
|
|
1351
|
+
var SETTINGS_LAUNCHER_TEXTS = ["设置", "Settings", "設定", "설정", "Einstellungen"];
|
|
1352
|
+
var SETTINGS_PANEL_ID = "plugins";
|
|
1353
|
+
var SETTINGS_BUNDLE_NAME = "dsh-quick-toc";
|
|
1354
|
+
function readNodeText(node) {
|
|
1355
|
+
if (node === null || node === undefined) return "";
|
|
1356
|
+
var text = node.textContent;
|
|
1357
|
+
return typeof text === "string" ? text : "";
|
|
1358
|
+
}
|
|
1359
|
+
function readNodeAttr(node, name) {
|
|
1360
|
+
if (node === null || node === undefined || typeof node.getAttribute !== "function") return "";
|
|
1361
|
+
var value = node.getAttribute(name);
|
|
1362
|
+
return value === null || value === undefined ? "" : String(value);
|
|
1363
|
+
}
|
|
1364
|
+
// A node's own label: whitespace collapsed, case folded — what an exact match
|
|
1365
|
+
// is compared against.
|
|
1366
|
+
function nodeLabel(node) {
|
|
1367
|
+
return readNodeText(node).replace(/\s+/g, " ").trim().toLowerCase();
|
|
1368
|
+
}
|
|
1369
|
+
// Whether the node itself is the thing a user clicks (not a list item that
|
|
1370
|
+
// merely CONTAINS the clickable).
|
|
1371
|
+
function isClickableSelf(node) {
|
|
1372
|
+
var tag = typeof node.tagName === "string" ? node.tagName.toLowerCase() : "";
|
|
1373
|
+
if (tag === "button" || tag === "a") return true;
|
|
1374
|
+
var role = readNodeAttr(node, "role").toLowerCase();
|
|
1375
|
+
return role === "button" || role === "tab" || role === "menuitem" || role === "link" || role === "option";
|
|
1376
|
+
}
|
|
1377
|
+
// Whether one element is a settings navigation cell that opens the Plugins
|
|
1378
|
+
// screen: the entry's own id as a data attribute first (the one form that does
|
|
1379
|
+
// not depend on a label's language), then an exactly labelled current cell.
|
|
1380
|
+
function isSettingsNavItem(node) {
|
|
1381
|
+
if (node === null || node === undefined || typeof node !== "object") return false;
|
|
1382
|
+
if (readNodeAttr(node, SETTINGS_NAV_ITEM_ATTR) === SETTINGS_NAV_ITEM_VALUE) return true;
|
|
1383
|
+
if (readNodeAttr(node, SETTINGS_NAV_CURRENT_ATTR) === "") return false;
|
|
1384
|
+
var label = nodeLabel(node);
|
|
1385
|
+
if (label === "") return false;
|
|
1386
|
+
for (var i = 0; i < SETTINGS_NAV_TEXTS.length; i++) {
|
|
1387
|
+
if (label === SETTINGS_NAV_TEXTS[i].toLowerCase()) return true;
|
|
1388
|
+
}
|
|
1389
|
+
return false;
|
|
1390
|
+
}
|
|
1391
|
+
// Whether one element is THIS plugin's own row in the Plugins list.
|
|
1392
|
+
//
|
|
1393
|
+
// The shipped id is the only evidence trusted on its own. The label route needs
|
|
1394
|
+
// a node that is clickable ITSELF — a container whose text happens to mention
|
|
1395
|
+
// the plugin (a session row, a message that names it) is never the entry, and
|
|
1396
|
+
// clicking "the first button inside it" is how the jump used to land somewhere
|
|
1397
|
+
// else entirely.
|
|
1398
|
+
function isPluginEntry(node) {
|
|
1399
|
+
if (node === null || node === undefined || typeof node !== "object") return false;
|
|
1400
|
+
if (readNodeAttr(node, PLUGIN_ITEM_ATTR) === PLUGIN_ITEM_VALUE) return true;
|
|
1401
|
+
if (!isClickableSelf(node)) return false;
|
|
1402
|
+
var label = nodeLabel(node);
|
|
1403
|
+
if (label === "") return false;
|
|
1404
|
+
for (var i = 0; i < PLUGIN_TEXTS.length; i++) {
|
|
1405
|
+
if (label === PLUGIN_TEXTS[i].toLowerCase()) return true;
|
|
1406
|
+
}
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
function isPluginConfigTab(node) {
|
|
1410
|
+
if (node === null || node === undefined || typeof node !== "object") return false;
|
|
1411
|
+
if (!isClickableSelf(node)) return false;
|
|
1412
|
+
var label = nodeLabel(node);
|
|
1413
|
+
if (label === "") return false;
|
|
1414
|
+
for (var i = 0; i < PLUGIN_CONFIG_TEXTS.length; i++) {
|
|
1415
|
+
if (label === PLUGIN_CONFIG_TEXTS[i].toLowerCase()) return true;
|
|
1416
|
+
}
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
function isSettingsLauncher(node) {
|
|
1420
|
+
if (node === null || node === undefined || typeof node !== "object") return false;
|
|
1421
|
+
if (readNodeAttr(node, "aria-haspopup") !== SETTINGS_LAUNCHER_HASPOPUP) return false;
|
|
1422
|
+
var label = (readNodeAttr(node, "aria-label") || readNodeAttr(node, "title") || readNodeText(node))
|
|
1423
|
+
.trim().toLowerCase();
|
|
1424
|
+
if (label === "") return false;
|
|
1425
|
+
for (var i = 0; i < SETTINGS_LAUNCHER_TEXTS.length; i++) {
|
|
1426
|
+
if (label.indexOf(SETTINGS_LAUNCHER_TEXTS[i].toLowerCase()) >= 0) return true;
|
|
1427
|
+
}
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
function dispatchSynthetic(node, type) {
|
|
1431
|
+
if (node === null || node === undefined || typeof node.dispatchEvent !== "function") return false;
|
|
1432
|
+
var win = typeof globalThis !== "undefined" ? globalThis : null;
|
|
1433
|
+
var View = win !== null && win.MouseEvent !== undefined ? win.MouseEvent : null;
|
|
1434
|
+
var event = null;
|
|
1435
|
+
try {
|
|
1436
|
+
event = View !== null
|
|
1437
|
+
? new View(type, { bubbles: true, cancelable: true, view: win })
|
|
1438
|
+
: { type: type, bubbles: true, cancelable: true, defaultPrevented: false, preventDefault: function () {} };
|
|
1439
|
+
} catch (e) {
|
|
1440
|
+
event = { type: type, bubbles: true, cancelable: true, defaultPrevented: false, preventDefault: function () {} };
|
|
1441
|
+
}
|
|
1442
|
+
try {
|
|
1443
|
+
node.dispatchEvent(event);
|
|
1444
|
+
return true;
|
|
1445
|
+
} catch (e) {
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
// Whether a node sits somewhere whose text names this plugin without being its
|
|
1450
|
+
// settings entry: our OWN panel, or the sidebar's session list (a conversation
|
|
1451
|
+
// titled "quick-toc 插件开发" is not the plugin's page).
|
|
1452
|
+
function isForeignNode(node) {
|
|
1453
|
+
if (node === null || node === undefined) return true;
|
|
1454
|
+
try {
|
|
1455
|
+
if (typeof node.closest === "function") {
|
|
1456
|
+
if (node.closest(".dqt-list, .dqt-header, .dqt-sheet-chrome") !== null) return true;
|
|
1457
|
+
if (node.closest('[role="treeview"], [role="tree"], [data-row-key]') !== null) return true;
|
|
1458
|
+
}
|
|
1459
|
+
} catch (e) { return false; }
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
// The element that opens a plugin's page from its entry card: the card itself is
|
|
1463
|
+
// a list item with no handler of its own, and the host renders the title button
|
|
1464
|
+
// inside it as the open gesture.
|
|
1465
|
+
function pluginCardTarget(card) {
|
|
1466
|
+
if (isClickableSelf(card)) return card;
|
|
1467
|
+
if (typeof card.querySelector === "function") {
|
|
1468
|
+
var button = null;
|
|
1469
|
+
try {
|
|
1470
|
+
button = card.querySelector("button");
|
|
1471
|
+
} catch (e) { button = null; }
|
|
1472
|
+
if (button !== null && button !== undefined) return button;
|
|
1473
|
+
}
|
|
1474
|
+
return card;
|
|
1475
|
+
}
|
|
1476
|
+
// THIS plugin's own row in the Plugins list, or null.
|
|
1477
|
+
//
|
|
1478
|
+
// Two shapes, in the order they can be trusted: the host-published id (the
|
|
1479
|
+
// shipped `data-plugin-item`, whose card carries the open gesture), then a node
|
|
1480
|
+
// that is clickable ITSELF and labelled exactly with one of this plugin's names
|
|
1481
|
+
// (a plugin that ships its own sidebar entry). Nothing else — a loose label
|
|
1482
|
+
// match on a container, then a click on whatever button sat inside it, is what
|
|
1483
|
+
// used to send this button somewhere else entirely.
|
|
1484
|
+
function settingsPluginEntry(doc) {
|
|
1485
|
+
if (doc === null || doc === undefined || typeof doc.querySelectorAll !== "function") return null;
|
|
1486
|
+
var card = null;
|
|
1487
|
+
try {
|
|
1488
|
+
if (typeof doc.querySelector === "function") {
|
|
1489
|
+
card = doc.querySelector("[" + PLUGIN_ITEM_ATTR + '="' + PLUGIN_ITEM_VALUE + '"]');
|
|
1490
|
+
}
|
|
1491
|
+
} catch (e) { card = null; }
|
|
1492
|
+
if (card !== null && card !== undefined && !isForeignNode(card)) return pluginCardTarget(card);
|
|
1493
|
+
var candidates = null;
|
|
1494
|
+
try {
|
|
1495
|
+
candidates = doc.querySelectorAll(PLUGIN_CARD_CANDIDATES);
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
1500
|
+
var node = candidates[i];
|
|
1501
|
+
if (isForeignNode(node)) continue;
|
|
1502
|
+
if (!isClickableSelf(node)) continue;
|
|
1503
|
+
if (!isPluginEntry(node)) continue;
|
|
1504
|
+
return node;
|
|
1505
|
+
}
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
// The shortest route to the Plugins screen, or null. Plans are plain clicks —
|
|
1509
|
+
// nothing here pokes at a framework's internals.
|
|
1510
|
+
function findSettingsEntry(doc, options) {
|
|
1511
|
+
if (doc === null || doc === undefined || typeof doc !== "object") return null;
|
|
1512
|
+
var opts = options !== null && typeof options === "object" ? options : {};
|
|
1513
|
+
var all = function () {
|
|
1514
|
+
var found = typeof doc.querySelectorAll === "function" ? doc.querySelectorAll("button") : null;
|
|
1515
|
+
return found === null || found === undefined ? [] : found;
|
|
1516
|
+
};
|
|
1517
|
+
var first = function (predicate) {
|
|
1518
|
+
var nodes = all();
|
|
1519
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1520
|
+
if (predicate(nodes[i])) return nodes[i];
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1523
|
+
};
|
|
1524
|
+
var cellWithAttr = function () {
|
|
1525
|
+
return first(function (node) {
|
|
1526
|
+
return readNodeAttr(node, SETTINGS_NAV_ITEM_ATTR) === SETTINGS_NAV_ITEM_VALUE;
|
|
1527
|
+
});
|
|
1528
|
+
};
|
|
1529
|
+
var cellByText = function () {
|
|
1530
|
+
return first(function (node) {
|
|
1531
|
+
return readNodeAttr(node, SETTINGS_NAV_CURRENT_ATTR) !== "" && isSettingsNavItem(node);
|
|
1532
|
+
});
|
|
1533
|
+
};
|
|
1534
|
+
var launcher = function () { return first(isSettingsLauncher); };
|
|
1535
|
+
var firstIn = function (selector, predicate) {
|
|
1536
|
+
var nodes = typeof doc.querySelectorAll === "function" ? doc.querySelectorAll(selector) : [];
|
|
1537
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1538
|
+
if (predicate(nodes[i])) return nodes[i];
|
|
1539
|
+
}
|
|
1540
|
+
return null;
|
|
1541
|
+
};
|
|
1542
|
+
var pluginConfigTab = function () { return firstIn(PLUGIN_TAB_CANDIDATES, isPluginConfigTab); };
|
|
1543
|
+
var sectionPlan = function (kind, why, node) {
|
|
1544
|
+
return {
|
|
1545
|
+
kind: kind,
|
|
1546
|
+
why: why,
|
|
1547
|
+
node: node,
|
|
1548
|
+
run: function () { return dispatchSynthetic(node, "click"); }
|
|
1549
|
+
};
|
|
1550
|
+
};
|
|
1551
|
+
// every route shares the tail: the Plugins cell, then THIS plugin's row
|
|
1552
|
+
// inside it, then its configuration tab when the host has one (a host that
|
|
1553
|
+
// shows the configuration inline has no such tab — not a failure)
|
|
1554
|
+
var withPluginEntry = function (plan) {
|
|
1555
|
+
var opened = plan.run;
|
|
1556
|
+
plan.run = function () {
|
|
1557
|
+
var reached = opened();
|
|
1558
|
+
if (typeof opts.retry !== "function") return reached;
|
|
1559
|
+
var entry = opts.retry(function () { return settingsPluginEntry(doc); });
|
|
1560
|
+
if (entry === null) return reached;
|
|
1561
|
+
return opts.retry(pluginConfigTab) || entry;
|
|
1562
|
+
};
|
|
1563
|
+
return plan;
|
|
1564
|
+
};
|
|
1565
|
+
// the plugin may ship its own SIDEBAR entry — clicking it opens the
|
|
1566
|
+
// configuration directly and there is no settings route to walk
|
|
1567
|
+
var sidebarEntry = settingsPluginEntry(doc);
|
|
1568
|
+
if (sidebarEntry !== null) {
|
|
1569
|
+
return {
|
|
1570
|
+
kind: "sidebar-entry",
|
|
1571
|
+
why: "the plugin ships its own entry; it opens the configuration directly",
|
|
1572
|
+
node: sidebarEntry,
|
|
1573
|
+
run: function () { return dispatchSynthetic(sidebarEntry, "click"); }
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
var canonical = cellWithAttr();
|
|
1577
|
+
if (canonical !== null) {
|
|
1578
|
+
return withPluginEntry(sectionPlan("section-attr", "the Plugins entry ships its own id as a data attribute", canonical));
|
|
1579
|
+
}
|
|
1580
|
+
var byText = cellByText();
|
|
1581
|
+
if (byText !== null) {
|
|
1582
|
+
return withPluginEntry(sectionPlan("section-text", "the Plugins entry was matched by its translated label", byText));
|
|
1583
|
+
}
|
|
1584
|
+
var opener = launcher();
|
|
1585
|
+
if (opener === null) return null;
|
|
1586
|
+
return withPluginEntry({
|
|
1587
|
+
kind: "launcher",
|
|
1588
|
+
why: "only the sidebar's settings launcher is on screen; the entry is picked after it opens",
|
|
1589
|
+
node: opener,
|
|
1590
|
+
run: function () {
|
|
1591
|
+
var reached = dispatchSynthetic(opener, "click");
|
|
1592
|
+
if (typeof opts.retry !== "function") return reached;
|
|
1593
|
+
return opts.retry(cellWithAttr) || opts.retry(cellByText) || reached;
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// The host context `apply` received: the settings button's navigation
|
|
1599
|
+
// services are looked up through it, never declared as dependencies (a
|
|
1600
|
+
// required inject would park this whole package on a host without them).
|
|
1601
|
+
var settingsHostCtx = null;
|
|
1602
|
+
function settingsService(name) {
|
|
1603
|
+
var ctx = settingsHostCtx;
|
|
1604
|
+
if (ctx === null || ctx === undefined) return null;
|
|
1605
|
+
try {
|
|
1606
|
+
if (typeof ctx.get === "function") {
|
|
1607
|
+
var found = ctx.get(name);
|
|
1608
|
+
if (found) return found;
|
|
1609
|
+
}
|
|
1610
|
+
} catch (e) { /* fall through to the property read */ }
|
|
1611
|
+
try {
|
|
1612
|
+
return ctx[name] || null;
|
|
1613
|
+
} catch (e) {
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
// Click THIS plugin's entry on the Plugins page once the page has mounted it.
|
|
1618
|
+
//
|
|
1619
|
+
// The card appears a commit or two after the panel switch, and the inventory
|
|
1620
|
+
// itself may still be loading, so the lookup polls briefly instead of giving up
|
|
1621
|
+
// on the first frame; twenty tries at 200 ms is well past a page paint and still
|
|
1622
|
+
// reads as one gesture.
|
|
1623
|
+
function clickSettingsEntry(done) {
|
|
1624
|
+
var tries = 0;
|
|
1625
|
+
var step = function () {
|
|
1626
|
+
tries += 1;
|
|
1627
|
+
var target = settingsPluginEntry(document);
|
|
1628
|
+
if (target !== null) {
|
|
1629
|
+
done(dispatchSynthetic(target, "click") === true);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (tries >= 20) { done(false); return; }
|
|
1633
|
+
setTimeout(step, 200);
|
|
1634
|
+
};
|
|
1635
|
+
step();
|
|
1636
|
+
}
|
|
1637
|
+
// The host's own navigation first: the Plugins page is a main panel the layout
|
|
1638
|
+
// service selects by id, and the entry inside it is the host's own card — the
|
|
1639
|
+
// same two steps a user performs by hand. Measured on 0.2.0-rc.2: both services
|
|
1640
|
+
// are published, and this plugin's card is `[data-plugin-item="quick-toc"]`.
|
|
1641
|
+
//
|
|
1642
|
+
// Order: select the Plugins panel and click THIS plugin's card; if the card never
|
|
1643
|
+
// mounts, ask `pluginNavigation.openBundle` for the package's own page; failing
|
|
1644
|
+
// both, the caller keeps the DOM chain.
|
|
1645
|
+
function openPluginSettingsPage(warn) {
|
|
1646
|
+
var nav = settingsService("pluginNavigation");
|
|
1647
|
+
var layout = settingsService("layout");
|
|
1648
|
+
var canBundle = nav !== null && nav !== undefined && typeof nav.openBundle === "function";
|
|
1649
|
+
var canPanel = layout !== null && layout !== undefined && typeof layout.selectPanel === "function";
|
|
1650
|
+
var openBundle = function () {
|
|
1651
|
+
if (!canBundle) return false;
|
|
1652
|
+
try {
|
|
1653
|
+
nav.openBundle(SETTINGS_BUNDLE_NAME);
|
|
1654
|
+
return true;
|
|
1655
|
+
} catch (e) {
|
|
1656
|
+
warn("the plugin navigation service refused the package");
|
|
1657
|
+
return false;
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
if (!canPanel) return openBundle();
|
|
1661
|
+
try {
|
|
1662
|
+
layout.selectPanel(SETTINGS_PANEL_ID);
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
return openBundle();
|
|
1665
|
+
}
|
|
1666
|
+
// the page mounts a commit after the panel switch: the card click waits for it
|
|
1667
|
+
clickSettingsEntry(function (clicked) {
|
|
1668
|
+
if (clicked) return;
|
|
1669
|
+
if (openBundle()) return;
|
|
1670
|
+
warn("the Plugins page never mounted this plugin's entry");
|
|
1671
|
+
});
|
|
1672
|
+
return true;
|
|
1673
|
+
}
|
|
1674
|
+
// Go to this plugin's own settings screen, with every route the host offers;
|
|
1675
|
+
// nothing is silent — a click that cannot land reports why.
|
|
1676
|
+
function openSettingsScreen(reason, report) {
|
|
1677
|
+
var tag = typeof reason === "string" && reason !== "" ? reason : "settings button";
|
|
1678
|
+
var warn = typeof report === "function" ? report : function () {};
|
|
1679
|
+
if (openPluginSettingsPage(function (detail) { warn(tag, detail); })) return true;
|
|
1680
|
+
var attempts = 0;
|
|
1681
|
+
var retry = function (select) {
|
|
1682
|
+
// the dialog mounts in one commit, and the row mounts one commit after
|
|
1683
|
+
// the cell; six frames is far more than that and still short enough that
|
|
1684
|
+
// a miss reads as "nothing happened"
|
|
1685
|
+
attempts += 1;
|
|
1686
|
+
if (attempts > 6) return null;
|
|
1687
|
+
var found = select();
|
|
1688
|
+
if (found === null) return null;
|
|
1689
|
+
dispatchSynthetic(found, "click");
|
|
1690
|
+
return found;
|
|
1691
|
+
};
|
|
1692
|
+
var plan = findSettingsEntry(document, { retry: retry });
|
|
1693
|
+
if (plan === null) {
|
|
1694
|
+
warn(tag, "no settings entry and no settings launcher on screen");
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
var taken = plan.run();
|
|
1698
|
+
return taken !== false && taken !== null;
|
|
1146
1699
|
}
|
|
1147
1700
|
|
|
1148
1701
|
// strict lookup: exact dataset match or CSS-escaped selector only —
|
|
@@ -1383,7 +1936,15 @@ window.__ModuleLoader__.load({
|
|
|
1383
1936
|
// closing outline + accent bar so the block reads as a finished shape.
|
|
1384
1937
|
groupTint: "rgba(79, 140, 255, 0.10)",
|
|
1385
1938
|
groupEdge: "rgba(79, 140, 255, 0.85)",
|
|
1386
|
-
groupEdgeSoft: "rgba(79, 140, 255, 0.35)"
|
|
1939
|
+
groupEdgeSoft: "rgba(79, 140, 255, 0.35)",
|
|
1940
|
+
// the curtain's bottom edge: a fixed mid-grey line that reads on both themes
|
|
1941
|
+
// (the l2 token is too faint to see against the conversation below)
|
|
1942
|
+
sheetEdge: "rgba(120, 120, 120, 0.5)",
|
|
1943
|
+
// the curtain's outer shadow: bottom only. The curtain meets its box edge to
|
|
1944
|
+
// edge at the left and right, so the negative spreads keep the vertical seams
|
|
1945
|
+
// free of shadow while the bottom — rounded corners included — lifts off the
|
|
1946
|
+
// conversation below.
|
|
1947
|
+
sheetShadow: "0 14px 22px -12px rgba(0, 0, 0, 0.35), 0 6px 12px -10px rgba(0, 0, 0, 0.25)"
|
|
1387
1948
|
};
|
|
1388
1949
|
|
|
1389
1950
|
// ---- theme scheme detection (for the panel's 3D inner shadow) ----
|
|
@@ -1400,27 +1961,27 @@ window.__ModuleLoader__.load({
|
|
|
1400
1961
|
if (body.hasAttribute && body.hasAttribute("data-ds-dark-theme")) return true;
|
|
1401
1962
|
var cls = body.className || "";
|
|
1402
1963
|
if (/dark|theme-dark/i.test(cls)) return true;
|
|
1403
|
-
|
|
1404
|
-
|
|
1964
|
+
// each probe is guarded on its own: a stub or a host DOM without
|
|
1965
|
+
// getAttribute must not abort the whole read and force the light answer
|
|
1966
|
+
if (typeof body.getAttribute === "function") {
|
|
1967
|
+
var attr = (body.getAttribute("data-theme") || body.getAttribute("data-color-scheme") || "").toLowerCase();
|
|
1968
|
+
if (attr === "dark" || /dark/.test(attr)) return true;
|
|
1969
|
+
}
|
|
1405
1970
|
}
|
|
1406
1971
|
if (typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches) return true;
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1972
|
+
if (typeof getComputedStyle === "function") {
|
|
1973
|
+
var root = body || document.documentElement;
|
|
1974
|
+
var cs = getComputedStyle(root).colorScheme;
|
|
1975
|
+
if (typeof cs === "string" && /dark/i.test(cs)) return true;
|
|
1976
|
+
var v = getComputedStyle(root).getPropertyValue("--dsw-alias-bg-base");
|
|
1977
|
+
var m = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(v);
|
|
1978
|
+
if (m) return (Number(m[1]) + Number(m[2]) + Number(m[3])) < 384;
|
|
1979
|
+
}
|
|
1413
1980
|
} catch (e) {}
|
|
1414
1981
|
return false;
|
|
1415
1982
|
}
|
|
1416
|
-
//
|
|
1417
|
-
//
|
|
1418
|
-
// dark shade. Outer shadow stays off (clip-path would clip it).
|
|
1419
|
-
function innerShadow() {
|
|
1420
|
-
return isDarkScheme()
|
|
1421
|
-
? "inset 0 0 0 1px rgba(255,255,255,0.16), inset 0 1px 0 rgba(255,255,255,0.12), inset 0 -10px 16px rgba(255,255,255,0.08)"
|
|
1422
|
-
: "inset 0 0 0 1px rgba(0,0,0,0.08), inset 0 2px 10px rgba(0,0,0,0.18)";
|
|
1423
|
-
}
|
|
1983
|
+
// (the inner bevel is gone with the outer shadow's return: the panel now lifts
|
|
1984
|
+
// off the conversation with a real shadow of its own — see the panel's style.)
|
|
1424
1985
|
|
|
1425
1986
|
// ---------- error boundary ----------
|
|
1426
1987
|
var ErrorBoundary = (function () {
|
|
@@ -1724,42 +2285,8 @@ window.__ModuleLoader__.load({
|
|
|
1724
2285
|
setLevels(next);
|
|
1725
2286
|
};
|
|
1726
2287
|
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
// plays a short shrink-back animation before unmounting. ----
|
|
1730
|
-
var _sLo = react.useState(false);
|
|
1731
|
-
var levelsOpen = _sLo[0];
|
|
1732
|
-
var setLevelsOpen = _sLo[1];
|
|
1733
|
-
var _sLc = react.useState(false);
|
|
1734
|
-
var levelsClosing = _sLc[0];
|
|
1735
|
-
var setLevelsClosing = _sLc[1];
|
|
1736
|
-
var closeLevels = function () {
|
|
1737
|
-
if (!levelsOpen || levelsClosing) return;
|
|
1738
|
-
setLevelsClosing(true);
|
|
1739
|
-
// unmount when the exit animation actually finishes (fallback timer in
|
|
1740
|
-
// case animationend never fires, e.g. display:none ancestors)
|
|
1741
|
-
var popped = document.querySelector(".dqt-levels-pop");
|
|
1742
|
-
var done = false;
|
|
1743
|
-
var finish = function () {
|
|
1744
|
-
if (done) return;
|
|
1745
|
-
done = true;
|
|
1746
|
-
if (popped && popped.removeEventListener) popped.removeEventListener("animationend", finish);
|
|
1747
|
-
setLevelsOpen(false);
|
|
1748
|
-
setLevelsClosing(false);
|
|
1749
|
-
};
|
|
1750
|
-
if (popped && popped.addEventListener) popped.addEventListener("animationend", finish);
|
|
1751
|
-
setTimeout(finish, 220);
|
|
1752
|
-
};
|
|
1753
|
-
react.useEffect(function () {
|
|
1754
|
-
if (!levelsOpen) return;
|
|
1755
|
-
var close = function (e) {
|
|
1756
|
-
var t = e.target;
|
|
1757
|
-
if (t && t.closest && (t.closest(".dqt-levels-pop") || t.closest(".dqt-levels-btn"))) return;
|
|
1758
|
-
closeLevels();
|
|
1759
|
-
};
|
|
1760
|
-
document.addEventListener("pointerdown", close);
|
|
1761
|
-
return function () { document.removeEventListener("pointerdown", close); };
|
|
1762
|
-
}, [levelsOpen, levelsClosing]);
|
|
2288
|
+
// (the heading-level picker popup is gone with the toolbar's level button:
|
|
2289
|
+
// the levels are a setting, and the settings button lands on that card row)
|
|
1763
2290
|
|
|
1764
2291
|
|
|
1765
2292
|
// ---- pagination: show the latest N groups; scrolling to the top loads older ----
|
|
@@ -1771,6 +2298,13 @@ window.__ModuleLoader__.load({
|
|
|
1771
2298
|
var panelRootRef = react.useRef(null); // the panel box itself (for its live height)
|
|
1772
2299
|
var didInitScroll = react.useRef(false);
|
|
1773
2300
|
var outlineTouchRef = react.useRef(0); // last time the user touched the outline
|
|
2301
|
+
// A scroll the panel performs itself (follow, reveal, open landing) stamps
|
|
2302
|
+
// this first, so the scroll event it causes is not mistaken for a reader
|
|
2303
|
+
// gesture (which would quiet the follow for 1.5s after every step it takes).
|
|
2304
|
+
var programScrollRef = react.useRef(0);
|
|
2305
|
+
var markProgramScroll = function (ms) {
|
|
2306
|
+
programScrollRef.current = Date.now() + (ms || 250);
|
|
2307
|
+
};
|
|
1774
2308
|
var lastScrollTopRef = react.useRef(0);
|
|
1775
2309
|
var lastHostClickRef = react.useRef(0); // throttle for the "load earlier" click
|
|
1776
2310
|
// Auto-load bookkeeping (see the paging block): at most one pull in flight, a beat
|
|
@@ -1878,8 +2412,10 @@ window.__ModuleLoader__.load({
|
|
|
1878
2412
|
var el = listRef.current;
|
|
1879
2413
|
if (!el) return;
|
|
1880
2414
|
var a = anchorAt(el); // the row at the top of the list right now
|
|
2415
|
+
// `a.top` is already relative to the list box (see anchorAt); `off` is kept
|
|
2416
|
+
// in that same frame so `restoreDrift` compares like with like
|
|
1881
2417
|
closedPosRef.current[which] = a
|
|
1882
|
-
? { idx: a.idx, off: a.top
|
|
2418
|
+
? { idx: a.idx, off: a.top }
|
|
1883
2419
|
: null;
|
|
1884
2420
|
};
|
|
1885
2421
|
var armRestore = function (which) {
|
|
@@ -2226,34 +2762,6 @@ window.__ModuleLoader__.load({
|
|
|
2226
2762
|
var listFade = _sQF[0];
|
|
2227
2763
|
var setListFade = _sQF[1];
|
|
2228
2764
|
var listFadeTimers = react.useRef({ out: null, in: null });
|
|
2229
|
-
var toggleQuestions = function () {
|
|
2230
|
-
if (listFadeTimers.current.out) clearTimeout(listFadeTimers.current.out);
|
|
2231
|
-
if (listFadeTimers.current.in) clearTimeout(listFadeTimers.current.in);
|
|
2232
|
-
// reduce motion is a real preference: swap without the two-phase fade
|
|
2233
|
-
var calm = false;
|
|
2234
|
-
try {
|
|
2235
|
-
calm = typeof window !== "undefined" && typeof window.matchMedia === "function"
|
|
2236
|
-
&& window.matchMedia("(prefers-reduced-motion: reduce)").matches === true;
|
|
2237
|
-
} catch (e) { calm = false; }
|
|
2238
|
-
if (calm) {
|
|
2239
|
-
setQuestionsOnly(function (cur) { return !cur; });
|
|
2240
|
-
setListFade("idle");
|
|
2241
|
-
return;
|
|
2242
|
-
}
|
|
2243
|
-
setListFade("out");
|
|
2244
|
-
listFadeTimers.current.out = setTimeout(function () {
|
|
2245
|
-
listFadeTimers.current.out = null;
|
|
2246
|
-
setQuestionsOnly(function (cur) { return !cur; });
|
|
2247
|
-
// entering questions-only makes the level filter meaningless: close its
|
|
2248
|
-
// popup rather than leave a disabled control's menu open over the list
|
|
2249
|
-
if (!questionsOnly && typeof closeLevels === "function") closeLevels();
|
|
2250
|
-
setListFade("in");
|
|
2251
|
-
listFadeTimers.current.in = setTimeout(function () {
|
|
2252
|
-
listFadeTimers.current.in = null;
|
|
2253
|
-
setListFade("idle");
|
|
2254
|
-
}, 260);
|
|
2255
|
-
}, 150);
|
|
2256
|
-
};
|
|
2257
2765
|
react.useEffect(function () {
|
|
2258
2766
|
return function () {
|
|
2259
2767
|
if (listFadeTimers.current.out) clearTimeout(listFadeTimers.current.out);
|
|
@@ -2455,21 +2963,68 @@ window.__ModuleLoader__.load({
|
|
|
2455
2963
|
var prevScope = _s19[0];
|
|
2456
2964
|
var setPrevScope = _s19[1];
|
|
2457
2965
|
var scopeTimerRef = react.useRef(null);
|
|
2458
|
-
//
|
|
2459
|
-
//
|
|
2460
|
-
|
|
2966
|
+
// recent searches, most-recent-first (browser-local; committing a search
|
|
2967
|
+
// fills it, an empty box offers it back as one-tap chips)
|
|
2968
|
+
var _sHist = react.useState(searchHistLoad);
|
|
2969
|
+
var searchHist = _sHist[0];
|
|
2970
|
+
var setSearchHist = _sHist[1];
|
|
2971
|
+
var commitSearchHist = function (q, scope) {
|
|
2972
|
+
// functional update: two commits in one batch must not drop each other
|
|
2973
|
+
setSearchHist(function (prev) { return searchHistRemember(prev, q, scope); });
|
|
2974
|
+
};
|
|
2975
|
+
// search scope: "title" (default) / "prompts" (only your own questions; an
|
|
2976
|
+
// empty box shows the questions view) / "full" (user messages + AI replies
|
|
2977
|
+
// in this session) / "cross" (the HOST's full-text index across every
|
|
2978
|
+
// visible session). ONE pill cycles the four, so a new scope costs no extra
|
|
2979
|
+
// button.
|
|
2980
|
+
var promptsView = searchScope === "prompts";
|
|
2461
2981
|
var scopeLabel = function (scope) {
|
|
2982
|
+
if (scope === "prompts") return T("search.scope.prompts");
|
|
2462
2983
|
if (scope === "full") return T("search.scope.full");
|
|
2463
2984
|
if (scope === "cross") return T("search.scope.cross");
|
|
2464
2985
|
return T("search.scope.title");
|
|
2465
2986
|
};
|
|
2987
|
+
// Entering or leaving the 提问 scope swaps the list between the outline and
|
|
2988
|
+
// the questions view — behind the same two-phase fade as of old: the list
|
|
2989
|
+
// fades out, the mode changes while it is invisible, then it fades back in,
|
|
2990
|
+
// so the two lists never blend into each other mid-swap.
|
|
2991
|
+
var swapQuestionsView = function (want) {
|
|
2992
|
+
if (listFadeTimers.current.out) clearTimeout(listFadeTimers.current.out);
|
|
2993
|
+
if (listFadeTimers.current.in) clearTimeout(listFadeTimers.current.in);
|
|
2994
|
+
listFadeTimers.current.out = null;
|
|
2995
|
+
listFadeTimers.current.in = null;
|
|
2996
|
+
if (want === questionsOnly && listFade === "idle") return;
|
|
2997
|
+
// reduce motion is a real preference: swap without the two-phase fade
|
|
2998
|
+
var calm = false;
|
|
2999
|
+
try {
|
|
3000
|
+
calm = typeof window !== "undefined" && typeof window.matchMedia === "function"
|
|
3001
|
+
&& window.matchMedia("(prefers-reduced-motion: reduce)").matches === true;
|
|
3002
|
+
} catch (e) { calm = false; }
|
|
3003
|
+
if (calm) {
|
|
3004
|
+
setQuestionsOnly(want);
|
|
3005
|
+
setListFade("idle");
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
setListFade("out");
|
|
3009
|
+
listFadeTimers.current.out = setTimeout(function () {
|
|
3010
|
+
listFadeTimers.current.out = null;
|
|
3011
|
+
setQuestionsOnly(want);
|
|
3012
|
+
setListFade("in");
|
|
3013
|
+
listFadeTimers.current.in = setTimeout(function () {
|
|
3014
|
+
listFadeTimers.current.in = null;
|
|
3015
|
+
setListFade("idle");
|
|
3016
|
+
}, 260);
|
|
3017
|
+
}, 150);
|
|
3018
|
+
};
|
|
2466
3019
|
var toggleScope = function () {
|
|
2467
|
-
var next = searchScope === "title" ? "
|
|
3020
|
+
var next = searchScope === "title" ? "prompts"
|
|
3021
|
+
: (searchScope === "prompts" ? "full" : (searchScope === "full" ? "cross" : "title"));
|
|
2468
3022
|
setPrevScope(searchScope);
|
|
2469
3023
|
setSearchScope(next);
|
|
2470
3024
|
setMatchIdx(0);
|
|
2471
3025
|
if (scopeTimerRef.current) clearTimeout(scopeTimerRef.current);
|
|
2472
3026
|
scopeTimerRef.current = setTimeout(function () { setPrevScope(null); }, 300);
|
|
3027
|
+
swapQuestionsView(next === "prompts");
|
|
2473
3028
|
};
|
|
2474
3029
|
// ---- fuzzy matching: an independent switch beside the scope pill --------
|
|
2475
3030
|
// Off (default) = the folded CONTIGUOUS match. On = subsequence matching, so
|
|
@@ -2834,7 +3389,13 @@ window.__ModuleLoader__.load({
|
|
|
2834
3389
|
if (isGhost && rk.top >= limit && !seenFull) { pick = k; pickTop = rk.top; seenFull = true; }
|
|
2835
3390
|
}
|
|
2836
3391
|
anchorHintRef.current = pick;
|
|
2837
|
-
|
|
3392
|
+
// RELATIVE to the list's own top edge — never a viewport-absolute value.
|
|
3393
|
+
// The panel and the curtain travel on `transform` (dock slides, the drop),
|
|
3394
|
+
// which moves every row and the list box by the same amount: a relative
|
|
3395
|
+
// offset cancels it out, while an absolute one reads the animation as
|
|
3396
|
+
// "content grew" and the compensation drags the list away from where the
|
|
3397
|
+
// follow just placed it.
|
|
3398
|
+
return { idx: boxes[pick].getAttribute("data-group-idx"), top: pickTop - lr.top };
|
|
2838
3399
|
};
|
|
2839
3400
|
// What the reader is looking at, and where it sits. Sampled by the scroll handler —
|
|
2840
3401
|
// so a host-side change landing mid-scroll is still caught — and right before any
|
|
@@ -2849,7 +3410,9 @@ window.__ModuleLoader__.load({
|
|
|
2849
3410
|
if (!a || !el || !el.isConnected) return false;
|
|
2850
3411
|
var node = el.querySelector('[data-group-idx="' + a.idx + '"]');
|
|
2851
3412
|
if (!node) { sampleAnchor(el); return false; }
|
|
2852
|
-
|
|
3413
|
+
// relative to the list box, the same frame the anchor was sampled in (see
|
|
3414
|
+
// anchorAt): viewport motion of the whole panel cancels out here
|
|
3415
|
+
var top = node.getBoundingClientRect().top - el.getBoundingClientRect().top;
|
|
2853
3416
|
var moved = el.scrollTop - a.scrollTop; // what the reader (or a jump) scrolled
|
|
2854
3417
|
var zf = liveZoom();
|
|
2855
3418
|
// Growth above the anchor moves it DOWN by exactly as much as the content grew; the
|
|
@@ -2869,6 +3432,7 @@ window.__ModuleLoader__.load({
|
|
|
2869
3432
|
return false;
|
|
2870
3433
|
}
|
|
2871
3434
|
var before = el.scrollTop;
|
|
3435
|
+
markProgramScroll(); // this write is ours, not the reader's
|
|
2872
3436
|
el.scrollTop = before + grew;
|
|
2873
3437
|
if (Math.abs(el.scrollTop - before) < 0.5) {
|
|
2874
3438
|
// clamped (the reader is already at one end): nothing to hold on to
|
|
@@ -3180,7 +3744,12 @@ window.__ModuleLoader__.load({
|
|
|
3180
3744
|
|
|
3181
3745
|
// scroll to the top edge also loads older groups (keeps the visual position)
|
|
3182
3746
|
var onListScroll = function (e) {
|
|
3183
|
-
|
|
3747
|
+
// A scroll the panel caused itself (see markProgramScroll) carries no
|
|
3748
|
+
// reader intent: it still updates the bottom state and the paging anchor,
|
|
3749
|
+
// but it must not refresh the follow's quiet window or spend the
|
|
3750
|
+
// "back to where I left" memory.
|
|
3751
|
+
var programmatic = Date.now() < programScrollRef.current;
|
|
3752
|
+
if (!programmatic) outlineTouchRef.current = Date.now();
|
|
3184
3753
|
// the scroll container itself: a real scroll event's `currentTarget` IS the list
|
|
3185
3754
|
// element, which is also what the paging logic reads geometry from
|
|
3186
3755
|
var el = e.currentTarget;
|
|
@@ -3191,7 +3760,7 @@ window.__ModuleLoader__.load({
|
|
|
3191
3760
|
if (hoverCard) hoverEnd(true);
|
|
3192
3761
|
}
|
|
3193
3762
|
lastScrollTopRef.current = el.scrollTop;
|
|
3194
|
-
syncBackBtn(el); // reached the remembered place by hand? the button is done
|
|
3763
|
+
if (!programmatic) syncBackBtn(el); // reached the remembered place by hand? the button is done
|
|
3195
3764
|
// Remember what the reader is looking at. Sampling on every scroll event is what
|
|
3196
3765
|
// lets a change the reader did NOT start — the host extending the window by itself
|
|
3197
3766
|
// — be compensated too: the layout effect knows where the anchored row was before
|
|
@@ -3612,7 +4181,7 @@ window.__ModuleLoader__.load({
|
|
|
3612
4181
|
// a turn the paged window has not loaded: searchable through the host
|
|
3613
4182
|
// outline's bounded previews, and clicking it pages the turn in first
|
|
3614
4183
|
if (g.ghost) {
|
|
3615
|
-
if (searchScope === "full") {
|
|
4184
|
+
if (searchScope === "full" || promptsView) {
|
|
3616
4185
|
var gtext = (g.userText || "") + "\n" + (g.preview || "");
|
|
3617
4186
|
var ng = countOccurrences(gtext, needle, fuzzy);
|
|
3618
4187
|
if (ng > 0) {
|
|
@@ -3621,13 +4190,15 @@ window.__ModuleLoader__.load({
|
|
|
3621
4190
|
title: previewText(g.userText || g.preview || "", 40),
|
|
3622
4191
|
level: 0, path: "", time: "",
|
|
3623
4192
|
snippet: snippetAround(g.preview || g.userText || "", fq, 36, fuzzy),
|
|
3624
|
-
ghost: true, turn: g.turn, seq: g.seq
|
|
4193
|
+
ghost: true, turn: g.turn, seq: g.seq, prompt: true
|
|
3625
4194
|
}, ng);
|
|
3626
4195
|
}
|
|
3627
4196
|
}
|
|
3628
4197
|
continue;
|
|
3629
4198
|
}
|
|
3630
|
-
|
|
4199
|
+
// the 提问 filter: only prompt rows survive (the old questions-only
|
|
4200
|
+
// toggle's job, now as a search filter)
|
|
4201
|
+
for (var j = 0; j < (promptsView ? 0 : g.headings.length); j++) {
|
|
3631
4202
|
var h = g.headings[j];
|
|
3632
4203
|
var nh = countOccurrences(h.title, needle, fuzzy);
|
|
3633
4204
|
if (nh > 0) {
|
|
@@ -3639,15 +4210,16 @@ window.__ModuleLoader__.load({
|
|
|
3639
4210
|
}, nh);
|
|
3640
4211
|
}
|
|
3641
4212
|
}
|
|
3642
|
-
if (searchScope === "full") {
|
|
4213
|
+
if (searchScope === "full" || promptsView) {
|
|
3643
4214
|
var nu = g.userKey && g.userFull ? countOccurrences(g.userFull, needle, fuzzy) : 0;
|
|
3644
4215
|
if (nu > 0) {
|
|
3645
4216
|
push({
|
|
3646
4217
|
gi: gi, key: g.userKey, idx: undefined, title: previewText(g.userFull, 40),
|
|
3647
|
-
level: 0, path: "", time: g.time, snippet: snippetAround(g.userFull, fq, 36, fuzzy)
|
|
4218
|
+
level: 0, path: "", time: g.time, snippet: snippetAround(g.userFull, fq, 36, fuzzy),
|
|
4219
|
+
prompt: true
|
|
3648
4220
|
}, nu);
|
|
3649
4221
|
}
|
|
3650
|
-
for (var m = 0; m < g.msgs.length; m++) {
|
|
4222
|
+
for (var m = 0; m < (promptsView ? 0 : g.msgs.length); m++) {
|
|
3651
4223
|
var msg = g.msgs[m];
|
|
3652
4224
|
if (!msg.text) continue;
|
|
3653
4225
|
var nm = countOccurrences(msg.text, needle, fuzzy);
|
|
@@ -3659,7 +4231,7 @@ window.__ModuleLoader__.load({
|
|
|
3659
4231
|
}
|
|
3660
4232
|
// a failed turn's provider message is searchable too — "timed out" must
|
|
3661
4233
|
// find the turns it killed
|
|
3662
|
-
if (g.failure && g.failure.message) {
|
|
4234
|
+
if (g.failure && g.failure.message && !promptsView) {
|
|
3663
4235
|
var nf = countOccurrences(g.failure.message, needle, fuzzy);
|
|
3664
4236
|
if (nf > 0) {
|
|
3665
4237
|
push({
|
|
@@ -3672,7 +4244,7 @@ window.__ModuleLoader__.load({
|
|
|
3672
4244
|
}
|
|
3673
4245
|
}
|
|
3674
4246
|
return rows;
|
|
3675
|
-
}, [groups, query, searchScope, headingPaths, fuzzy]);
|
|
4247
|
+
}, [groups, query, searchScope, headingPaths, fuzzy, promptsView]);
|
|
3676
4248
|
// ---- auto-follow: which turn is the reader looking at -------------------
|
|
3677
4249
|
// This used to hang off ONE scroll listener bound to the scrollport element
|
|
3678
4250
|
// captured when the effect ran. DSH can replace that element (a jump that
|
|
@@ -3759,7 +4331,10 @@ window.__ModuleLoader__.load({
|
|
|
3759
4331
|
var giSave = actives[0];
|
|
3760
4332
|
if (groups[giSave]) savePosition(groups[giSave]);
|
|
3761
4333
|
// pause right after the reader touched the outline themselves —
|
|
3762
|
-
// otherwise paging older turns gets yanked back to the newest group
|
|
4334
|
+
// otherwise paging older turns gets yanked back to the newest group.
|
|
4335
|
+
// (A scroll the follow performs itself must not count as a touch: it
|
|
4336
|
+
// stamps programScrollRef first, and the scroll handler lets a
|
|
4337
|
+
// programmatic scroll through without refreshing the quiet window.)
|
|
3763
4338
|
if (Date.now() - outlineTouchRef.current <= 1500) return;
|
|
3764
4339
|
var gi0 = actives[0];
|
|
3765
4340
|
setVisibleCount(function (prev) {
|
|
@@ -3779,7 +4354,13 @@ window.__ModuleLoader__.load({
|
|
|
3779
4354
|
// Aligning to the top needs no clamp at all — the list's own end does the work.
|
|
3780
4355
|
// (Rects are visual; the factor turns the delta back into the scroller's units.)
|
|
3781
4356
|
var delta = (node.getBoundingClientRect().top - el.getBoundingClientRect().top) / liveZoom();
|
|
3782
|
-
if (Math.abs(delta) > 1)
|
|
4357
|
+
if (Math.abs(delta) > 1) {
|
|
4358
|
+
markProgramScroll();
|
|
4359
|
+
el.scrollTop = el.scrollTop + delta;
|
|
4360
|
+
// the anchor must now describe THIS position, or the height-change
|
|
4361
|
+
// compensation re-aligns to where the list was before the follow moved it
|
|
4362
|
+
sampleAnchor(el);
|
|
4363
|
+
}
|
|
3783
4364
|
};
|
|
3784
4365
|
followFrame = requestAnimationFrame(function () { followFrame = requestAnimationFrame(place); });
|
|
3785
4366
|
followTimer = setTimeout(place, 90); // the row may only exist after the expansion commits
|
|
@@ -3811,41 +4392,50 @@ window.__ModuleLoader__.load({
|
|
|
3811
4392
|
for (var gi = 0; gi < groups.length; gi++) {
|
|
3812
4393
|
var g = groups[gi];
|
|
3813
4394
|
if (g.ghost) {
|
|
3814
|
-
if (searchScope === "full") {
|
|
4395
|
+
if (searchScope === "full" || promptsView) {
|
|
3815
4396
|
var gtext = (g.userText || "") + "\n" + (g.preview || "");
|
|
3816
4397
|
var ng = countOccurrences(gtext, needle, fuzzy);
|
|
3817
4398
|
for (var cg = 0; cg < ng; cg++) {
|
|
3818
|
-
out.push({ gi: gi, title: g.userText || "", key: "", idx: undefined, ghost: true, turn: g.turn, seq: g.seq });
|
|
4399
|
+
out.push({ gi: gi, title: g.userText || "", key: "", idx: undefined, ghost: true, turn: g.turn, seq: g.seq, prompt: true });
|
|
3819
4400
|
}
|
|
3820
4401
|
}
|
|
3821
4402
|
continue;
|
|
3822
4403
|
}
|
|
3823
|
-
|
|
4404
|
+
// the 提问 filter keeps only the prompts: heading and reply hits stay
|
|
4405
|
+
// out of the stepping list, so n/N counts what the results show
|
|
4406
|
+
for (var j = 0; j < (promptsView ? 0 : g.headings.length); j++) {
|
|
3824
4407
|
var h = g.headings[j];
|
|
3825
4408
|
var n = countOccurrences(h.title, needle, fuzzy);
|
|
3826
4409
|
for (var c = 0; c < n; c++) {
|
|
3827
4410
|
out.push({ gi: gi, title: h.title, key: h.key, idx: h.idx });
|
|
3828
4411
|
}
|
|
3829
4412
|
}
|
|
3830
|
-
if (searchScope === "full") {
|
|
4413
|
+
if (searchScope === "full" || promptsView) {
|
|
3831
4414
|
if (g.userKey && g.userFull) {
|
|
3832
4415
|
var nu = countOccurrences(g.userFull, needle, fuzzy);
|
|
3833
|
-
|
|
3834
|
-
|
|
4416
|
+
if (nu > 0) {
|
|
4417
|
+
// one preview per message, not one per occurrence
|
|
4418
|
+
var uTitle = previewText(g.userFull, 30);
|
|
4419
|
+
for (var cu = 0; cu < nu; cu++) {
|
|
4420
|
+
out.push({ gi: gi, title: uTitle, key: g.userKey, idx: undefined, prompt: true });
|
|
4421
|
+
}
|
|
3835
4422
|
}
|
|
3836
4423
|
}
|
|
3837
|
-
for (var m = 0; m < g.msgs.length; m++) {
|
|
4424
|
+
for (var m = 0; m < (promptsView ? 0 : g.msgs.length); m++) {
|
|
3838
4425
|
var msg = g.msgs[m];
|
|
3839
4426
|
if (!msg.text) continue;
|
|
3840
4427
|
var nm = countOccurrences(msg.text, needle, fuzzy);
|
|
3841
|
-
|
|
3842
|
-
|
|
4428
|
+
if (nm > 0) {
|
|
4429
|
+
var mTitle = previewText(msg.text, 30);
|
|
4430
|
+
for (var cm = 0; cm < nm; cm++) {
|
|
4431
|
+
out.push({ gi: gi, title: mTitle, key: msg.key, idx: undefined });
|
|
4432
|
+
}
|
|
3843
4433
|
}
|
|
3844
4434
|
}
|
|
3845
4435
|
}
|
|
3846
4436
|
}
|
|
3847
4437
|
return out;
|
|
3848
|
-
}, [groups, query, searchScope, fuzzy]);
|
|
4438
|
+
}, [groups, query, searchScope, fuzzy, promptsView]);
|
|
3849
4439
|
|
|
3850
4440
|
// which result row holds the current (n/N) occurrence (needs `matches`)
|
|
3851
4441
|
var activeResultRow = -1;
|
|
@@ -3974,6 +4564,7 @@ window.__ModuleLoader__.load({
|
|
|
3974
4564
|
return;
|
|
3975
4565
|
}
|
|
3976
4566
|
pendingLocate = { sessionId: hitSessionId, query: query.trim(), at: Date.now() };
|
|
4567
|
+
if (query.trim() !== "") commitSearchHist(query.trim(), searchScope);
|
|
3977
4568
|
try {
|
|
3978
4569
|
openSessionById(hitSessionId);
|
|
3979
4570
|
} catch (e) {
|
|
@@ -4030,7 +4621,7 @@ window.__ModuleLoader__.load({
|
|
|
4030
4621
|
searchPosRef.current = q;
|
|
4031
4622
|
if (!q) { setHint(""); return; }
|
|
4032
4623
|
var el = listRef.current;
|
|
4033
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
4624
|
+
if (el) { markProgramScroll(); el.scrollTop = el.scrollHeight; }
|
|
4034
4625
|
// cross-session scope: the list is host hits, so neither the n/N cursor nor
|
|
4035
4626
|
// the "scroll up for older messages" hint applies
|
|
4036
4627
|
if (searchScope === "cross") { setHint(""); return; }
|
|
@@ -4088,7 +4679,7 @@ window.__ModuleLoader__.load({
|
|
|
4088
4679
|
if (!el) return;
|
|
4089
4680
|
var tight = el.scrollWidth > el.clientWidth + 1;
|
|
4090
4681
|
setCrowded(function (prev) { return prev === tight ? prev : tight; });
|
|
4091
|
-
}, [open, panelW, zoom, dock,
|
|
4682
|
+
}, [open, panelW, zoom, dock, searchOpen, groups.length]);
|
|
4092
4683
|
react.useEffect(function () {
|
|
4093
4684
|
var el = headerRef.current;
|
|
4094
4685
|
if (!el) return;
|
|
@@ -4266,11 +4857,13 @@ window.__ModuleLoader__.load({
|
|
|
4266
4857
|
if (atBottomRef.current) {
|
|
4267
4858
|
if (!sheetOpen) return undefined;
|
|
4268
4859
|
anchorRef.current = null; // no compensation against a stale anchor
|
|
4860
|
+
markProgramScroll();
|
|
4269
4861
|
el.scrollTop = el.scrollHeight;
|
|
4270
4862
|
syncAtBottom(el);
|
|
4271
4863
|
var again = requestAnimationFrame(function () { // ...and once more, after layout
|
|
4272
4864
|
var el2 = listRef.current;
|
|
4273
4865
|
if (!el2 || !atBottomRef.current) return;
|
|
4866
|
+
markProgramScroll();
|
|
4274
4867
|
el2.scrollTop = el2.scrollHeight;
|
|
4275
4868
|
syncAtBottom(el2);
|
|
4276
4869
|
sampleAnchor(el2);
|
|
@@ -4295,6 +4888,32 @@ window.__ModuleLoader__.load({
|
|
|
4295
4888
|
return landOnReadingRow();
|
|
4296
4889
|
}, [open]);
|
|
4297
4890
|
|
|
4891
|
+
// Long-list virtualization must not run while a surface is ANIMATING. The
|
|
4892
|
+
// skip state of `content-visibility` is re-evaluated on the browser's own
|
|
4893
|
+
// layout passes, and the panel's open slide is a compositor-driven
|
|
4894
|
+
// transform — so a skipped list can stay blank through the whole slide and
|
|
4895
|
+
// only paint when it stops ("opens white, the text pops in when it lands").
|
|
4896
|
+
// The hold arms the moment the surface state flips and is released after
|
|
4897
|
+
// the animation window; the release's own re-render re-arms the skip.
|
|
4898
|
+
var virtReleaseRef = react.useRef(null);
|
|
4899
|
+
var virtHoldUntilRef = react.useRef(0);
|
|
4900
|
+
var virtSlot = react.useState(0);
|
|
4901
|
+
// the tick's VALUE is never read: the release's setter exists for its
|
|
4902
|
+
// re-render, which re-arms the skip after the animation window
|
|
4903
|
+
var setVirtTick = virtSlot[1];
|
|
4904
|
+
var surfaceKey = (open && !panelAside ? 1 : 0) + (sheetOpen ? 2 : 0);
|
|
4905
|
+
if (virtReleaseRef.current === null) {
|
|
4906
|
+
virtReleaseRef.current = surfaceKey; // the first paint is not an animation
|
|
4907
|
+
} else if (virtReleaseRef.current !== surfaceKey) {
|
|
4908
|
+
virtReleaseRef.current = surfaceKey;
|
|
4909
|
+
virtHoldUntilRef.current = Date.now() + VIRT_HOLD_MS;
|
|
4910
|
+
setTimeout(function () {
|
|
4911
|
+
virtHoldUntilRef.current = 0;
|
|
4912
|
+
setVirtTick(function (v) { return v + 1; });
|
|
4913
|
+
}, VIRT_HOLD_MS + 40);
|
|
4914
|
+
}
|
|
4915
|
+
var virtHold = Date.now() < virtHoldUntilRef.current;
|
|
4916
|
+
|
|
4298
4917
|
// ---- nothing to show without headings ----
|
|
4299
4918
|
if (groups.length === 0) return null;
|
|
4300
4919
|
|
|
@@ -4810,7 +5429,14 @@ window.__ModuleLoader__.load({
|
|
|
4810
5429
|
if (!node) return false;
|
|
4811
5430
|
// the rects are visual: divide by the live factor before writing a scroll offset
|
|
4812
5431
|
var delta = (node.getBoundingClientRect().top - el.getBoundingClientRect().top) / liveZoom();
|
|
4813
|
-
if (Math.abs(delta) > 0.5)
|
|
5432
|
+
if (Math.abs(delta) > 0.5) {
|
|
5433
|
+
markProgramScroll();
|
|
5434
|
+
el.scrollTop = el.scrollTop + delta;
|
|
5435
|
+
}
|
|
5436
|
+
// Either way this placement is the reader's new reality: the anchor must not
|
|
5437
|
+
// drag the list back to where it sat before (the curtain's re-layout makes the
|
|
5438
|
+
// old anchor look "pushed" and the compensation would undo the landing).
|
|
5439
|
+
sampleAnchor(el);
|
|
4814
5440
|
return true;
|
|
4815
5441
|
};
|
|
4816
5442
|
var revealGroupInOutline = function (gi) {
|
|
@@ -4873,6 +5499,8 @@ window.__ModuleLoader__.load({
|
|
|
4873
5499
|
var goToRow = function (i) {
|
|
4874
5500
|
var start = 0;
|
|
4875
5501
|
for (var k = 0; k < i && k < resultRows.length; k++) start += resultRows[k].count;
|
|
5502
|
+
// picking a result is a committed search too
|
|
5503
|
+
if (query.trim() !== "") commitSearchHist(query.trim(), searchScope);
|
|
4876
5504
|
goToMatch(start);
|
|
4877
5505
|
};
|
|
4878
5506
|
|
|
@@ -4934,7 +5562,13 @@ window.__ModuleLoader__.load({
|
|
|
4934
5562
|
// the curtain is wide: rows switch to a one-line, multi-part layout that
|
|
4935
5563
|
// puts the section's opening text out to the right instead of under the
|
|
4936
5564
|
// title (see renderItem / renderResultRow)
|
|
4937
|
-
wide: sheetOpen
|
|
5565
|
+
wide: sheetOpen,
|
|
5566
|
+
// compact density: tighten the row rhythm so more of the outline is on
|
|
5567
|
+
// screen (see renderGroups / renderGroupHeader / renderItem)
|
|
5568
|
+
dense: prefs.density === "compact",
|
|
5569
|
+
// the virtualization hold: while a surface animates, group boxes must not
|
|
5570
|
+
// skip their content (see the hold block above the early return)
|
|
5571
|
+
virtHold: virtHold
|
|
4938
5572
|
};
|
|
4939
5573
|
|
|
4940
5574
|
// ---- keyboard cursor plumbing -------------------------------------------
|
|
@@ -5117,7 +5751,12 @@ window.__ModuleLoader__.load({
|
|
|
5117
5751
|
// reader reported. A transform is handed to the compositor, so the animation no longer
|
|
5118
5752
|
// depends on how big the list is.
|
|
5119
5753
|
var panelTransition = (zoomJustChanged ? "" : "transform " + slide + ", ")
|
|
5120
|
-
+ "clip-path " + slide + ",
|
|
5754
|
+
+ "clip-path " + slide + ", box-shadow " + slide + ", opacity "
|
|
5755
|
+
// the fade is short on both legs: the tuck line and the emerge line read
|
|
5756
|
+
// as the SAME hard line — a slow fade-in made the panel hover out of thin
|
|
5757
|
+
// air instead of appearing from behind that line (0.45s was the softer
|
|
5758
|
+
// entrance, and the one thing still different between the two directions)
|
|
5759
|
+
+ (panelAway ? "0.14s ease 0.26s" : "0.16s ease");
|
|
5121
5760
|
|
|
5122
5761
|
// clip the panel at the dock edge while collapsed, so sliding away looks
|
|
5123
5762
|
// like being covered by the sidebar (the sidebar stays untouched):
|
|
@@ -5134,6 +5773,11 @@ window.__ModuleLoader__.load({
|
|
|
5134
5773
|
// against a `panelW + 16` travel the edge drifted 8px past that line, which the
|
|
5135
5774
|
// reader saw as the panel tucking in slightly left of where it comes back out.
|
|
5136
5775
|
var travelPx = Math.abs(openOffset - parkedOffset);
|
|
5776
|
+
// The wipe inset is the TRAVEL distance (see below): its edge must sit exactly
|
|
5777
|
+
// on the panel's open line through the whole slide. It stays a zero inset while
|
|
5778
|
+
// open — the outer shadow does NOT ride this clip (a negative bleed here made
|
|
5779
|
+
// the wipe edge drift 24px through the slide); the shadow lives on the panel's
|
|
5780
|
+
// twin element instead.
|
|
5137
5781
|
var panelClip = "inset(0 0 0 0px)";
|
|
5138
5782
|
if (!geoOpen) {
|
|
5139
5783
|
if (dockRight) {
|
|
@@ -5149,7 +5793,13 @@ window.__ModuleLoader__.load({
|
|
|
5149
5793
|
// readable whenever the pointer was elsewhere. Readability now comes from
|
|
5150
5794
|
// CONTRAST on the group being read (tinted row + accent bar, see
|
|
5151
5795
|
// renderGroups) instead of from fading everything else away.
|
|
5152
|
-
|
|
5796
|
+
//
|
|
5797
|
+
// NOTE the open/close legs do NOT fade: visibility past the tuck line is the
|
|
5798
|
+
// clip's call alone ("once it crosses the line it is gone"), and an opacity
|
|
5799
|
+
// ramp over the slide only made the two directions read different lines.
|
|
5800
|
+
// While parked the wipe covers the box entirely, so the resting opacity can
|
|
5801
|
+
// simply stay up.
|
|
5802
|
+
var panelOpacity = chatViewActive ? (hovered ? 1 : 0.72) : 0;
|
|
5153
5803
|
|
|
5154
5804
|
// ---- toolbar buttons -----------------------------------------------------
|
|
5155
5805
|
// ONE instance of each, placed either in the docked header row or in the
|
|
@@ -5205,55 +5855,40 @@ window.__ModuleLoader__.load({
|
|
|
5205
5855
|
}
|
|
5206
5856
|
return react_jsx_runtime.jsx("button", props);
|
|
5207
5857
|
};
|
|
5208
|
-
//
|
|
5209
|
-
//
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
(
|
|
5858
|
+
// This plugin's own settings: the Host's own Plugins-entry pinwheel —
|
|
5859
|
+
// copied path for path from dsh-fonttune's settings button (its 16-unit
|
|
5860
|
+
// box, 1.8 stroke, currentColor) — and the SAME jump it performs: open the
|
|
5861
|
+
// host's settings on THIS plugin's page (see openSettingsScreen).
|
|
5862
|
+
var settingsBtnEl = toolBtn("settings", "dqt-settings-btn",
|
|
5863
|
+
function () {
|
|
5864
|
+
openSettingsScreen("panel head", function (tag, detail) {
|
|
5865
|
+
try {
|
|
5866
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
5867
|
+
console.warn("[dsh-quick-toc] could not open the plugin settings (" + tag + ": " + detail + ")");
|
|
5868
|
+
}
|
|
5869
|
+
} catch (e) { /* a host without a console still gets the banner below */ }
|
|
5870
|
+
showBanner(T("settings.missing"));
|
|
5871
|
+
});
|
|
5872
|
+
},
|
|
5873
|
+
T("settings.open"), false, false,
|
|
5214
5874
|
react_jsx_runtime.jsx("svg", {
|
|
5215
|
-
width:
|
|
5216
|
-
height:
|
|
5217
|
-
viewBox: "0 0
|
|
5875
|
+
width: tbIconLg,
|
|
5876
|
+
height: tbIconLg,
|
|
5877
|
+
viewBox: "0 0 16 16",
|
|
5218
5878
|
fill: "none",
|
|
5219
5879
|
stroke: "currentColor",
|
|
5220
|
-
strokeWidth:
|
|
5221
|
-
strokeLinecap: "round",
|
|
5880
|
+
strokeWidth: 1.8,
|
|
5222
5881
|
style: { display: "block" },
|
|
5223
5882
|
children: [
|
|
5224
|
-
react_jsx_runtime.jsx("
|
|
5225
|
-
react_jsx_runtime.jsx("
|
|
5226
|
-
react_jsx_runtime.jsx("
|
|
5883
|
+
react_jsx_runtime.jsx("path", { d: "M7.84457 5.06199C11.6605 4.93876 14.7962 6.14848 14.8484 7.76397C14.8875 8.97461 13.1838 10.0696 10.7215 10.5942" }),
|
|
5884
|
+
react_jsx_runtime.jsx("path", { d: "M5.12742 8.07731C5.00419 4.26138 6.21391 1.12568 7.8294 1.07351C9.04004 1.03441 10.135 2.73808 10.6596 5.20037" }),
|
|
5885
|
+
react_jsx_runtime.jsx("path", { d: "M8.02457 10.6802C4.20865 10.8034 1.07294 9.5937 1.02077 7.97821C0.981678 6.76758 2.68535 5.67262 5.14763 5.14798" }),
|
|
5886
|
+
react_jsx_runtime.jsx("path", { d: "M10.7476 7.89535C10.8708 11.7113 9.66109 14.847 8.0456 14.8991C6.83496 14.9382 5.74 13.2346 5.21536 10.7723" })
|
|
5227
5887
|
]
|
|
5228
5888
|
})
|
|
5229
5889
|
);
|
|
5230
|
-
// questions-only
|
|
5231
|
-
//
|
|
5232
|
-
var questionsBtnEl = toolBtn("questions", "dqt-questions-btn", toggleQuestions,
|
|
5233
|
-
questionsOnly ? T("view.questions.tipOn") : T("view.questions.tipOff"),
|
|
5234
|
-
questionsOnly, false,
|
|
5235
|
-
react_jsx_runtime.jsx("svg", {
|
|
5236
|
-
width: tbIcon,
|
|
5237
|
-
height: tbIcon,
|
|
5238
|
-
viewBox: "0 0 14 14",
|
|
5239
|
-
fill: "none",
|
|
5240
|
-
stroke: "currentColor",
|
|
5241
|
-
strokeWidth: 2.2,
|
|
5242
|
-
strokeLinecap: "round",
|
|
5243
|
-
strokeLinejoin: "round",
|
|
5244
|
-
style: { display: "block" },
|
|
5245
|
-
children: [
|
|
5246
|
-
// A slightly LARGER bubble (11.4 x 7.7), and the whole glyph sits a touch BELOW the
|
|
5247
|
-
// box centre on purpose: the bubble body is the visual mass while the tail hangs
|
|
5248
|
-
// off the bottom-left, so a geometrically centred bubble reads as top-heavy. The
|
|
5249
|
-
// stroke bbox is 1.4..13.8 (centre 7.6, i.e. 0.6 low) and the horizontal centre is
|
|
5250
|
-
// still exactly 7.
|
|
5251
|
-
react_jsx_runtime.jsx("rect", { x: 1.3, y: 2.5, width: 11.4, height: 7.7, rx: 1.7 }),
|
|
5252
|
-
react_jsx_runtime.jsx("path", { d: "M4.5 10.2v2.5l2.9-2.5" })
|
|
5253
|
-
]
|
|
5254
|
-
}),
|
|
5255
|
-
{ "data-questions": questionsOnly ? "on" : "off" }
|
|
5256
|
-
);
|
|
5890
|
+
// questions-only lives in the SEARCH ROW now (its 提问 filter pill): the
|
|
5891
|
+
// toolbar lost the standalone toggle when the search grew the same job.
|
|
5257
5892
|
// magnifier (SVG, matches the other buttons' style)
|
|
5258
5893
|
var searchBtnEl = toolBtn("search", "dqt-search-btn",
|
|
5259
5894
|
function () { searchOpen ? closeSearch() : openSearch(); },
|
|
@@ -5297,69 +5932,8 @@ window.__ModuleLoader__.load({
|
|
|
5297
5932
|
]
|
|
5298
5933
|
})
|
|
5299
5934
|
);
|
|
5300
|
-
// heading-level picker
|
|
5301
|
-
//
|
|
5302
|
-
// the panel box in the docked layout (the header row scrolls sideways, so a
|
|
5303
|
-
// child of it would be clipped). Open/close animate from the button's center;
|
|
5304
|
-
// the animation lives in the injected stylesheet (classes, not inline
|
|
5305
|
-
// animation) so React re-renders never restart or cancel it.
|
|
5306
|
-
var buildLevelsPop = function (inSheet) {
|
|
5307
|
-
// on the curtain the six levels are ONE spread-out row (there is room);
|
|
5308
|
-
// in the narrow docked panel they have to wrap, and they are capped by the
|
|
5309
|
-
// panel's own width rather than by the shrink-to-fit toolbar wrapper
|
|
5310
|
-
var popW = inSheet ? "min(336px, 74vw)" : undefined;
|
|
5311
|
-
return react_jsx_runtime.jsx("div", {
|
|
5312
|
-
className: "dqt-levels-pop" + (levelsClosing ? " dqt-levels-pop-closing" : ""),
|
|
5313
|
-
style: {
|
|
5314
|
-
position: "absolute",
|
|
5315
|
-
top: inSheet ? "calc(100% + 8px)" : 40,
|
|
5316
|
-
left: inSheet ? 0 : 10,
|
|
5317
|
-
zIndex: 30,
|
|
5318
|
-
display: "flex",
|
|
5319
|
-
alignItems: "center",
|
|
5320
|
-
flexWrap: inSheet ? "nowrap" : "wrap",
|
|
5321
|
-
gap: 4,
|
|
5322
|
-
padding: 8,
|
|
5323
|
-
background: C.panelBg,
|
|
5324
|
-
border: "1px solid " + C.panelBorder,
|
|
5325
|
-
borderRadius: 10,
|
|
5326
|
-
cornerShape: "round",
|
|
5327
|
-
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.28), 0 2px 6px rgba(0, 0, 0, 0.18)",
|
|
5328
|
-
width: popW,
|
|
5329
|
-
maxWidth: inSheet ? popW : "calc(100% - 20px)",
|
|
5330
|
-
// the button's center sits 12px (dock) / 16px (curtain) inside the popup
|
|
5331
|
-
transformOrigin: (inSheet ? tbSize / 2 : 12) + "px top"
|
|
5332
|
-
},
|
|
5333
|
-
children: [1, 2, 3, 4, 5, 6].map(function (lv) {
|
|
5334
|
-
var active = !!levelSet[lv];
|
|
5335
|
-
return react_jsx_runtime.jsx("button", {
|
|
5336
|
-
onClick: function () { toggleLevel(lv); },
|
|
5337
|
-
title: active ? T("levels.hide") + lv : T("levels.show") + lv,
|
|
5338
|
-
style: {
|
|
5339
|
-
height: inSheet ? 32 : 22,
|
|
5340
|
-
flex: inSheet ? "1 1 0" : "0 0 auto",
|
|
5341
|
-
minWidth: 0,
|
|
5342
|
-
padding: inSheet ? 0 : "0 7px",
|
|
5343
|
-
border: "none",
|
|
5344
|
-
borderRadius: 6,
|
|
5345
|
-
cornerShape: "round",
|
|
5346
|
-
cursor: "pointer",
|
|
5347
|
-
fontSize: inSheet ? 14.5 : 11,
|
|
5348
|
-
lineHeight: (inSheet ? 32 : 22) + "px",
|
|
5349
|
-
fontFamily: "inherit",
|
|
5350
|
-
background: active
|
|
5351
|
-
? C.chip
|
|
5352
|
-
: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
5353
|
-
color: active ? C.accent : C.muted,
|
|
5354
|
-
fontWeight: active ? 600 : 400
|
|
5355
|
-
},
|
|
5356
|
-
children: "H" + lv
|
|
5357
|
-
}, "lv" + lv);
|
|
5358
|
-
})
|
|
5359
|
-
});
|
|
5360
|
-
};
|
|
5361
|
-
var levelsPopDockEl = (!sheetOpen && (levelsOpen || levelsClosing)) ? buildLevelsPop(false) : null;
|
|
5362
|
-
var levelsPopSheetEl = (sheetOpen && (levelsOpen || levelsClosing)) ? buildLevelsPop(true) : null;
|
|
5935
|
+
// (the heading-level picker popup went with the toolbar's level button: the
|
|
5936
|
+
// levels are a setting now, and the settings button lands on that row)
|
|
5363
5937
|
// dock toggle: the triangle tips toward the side it will move TO. Drawn as a path rather
|
|
5364
5938
|
// than the ◀/▶ character — a font's triangles have sharp corners and a weight that
|
|
5365
5939
|
// differs per platform, while this one carries the same little rounding as the two
|
|
@@ -5381,57 +5955,9 @@ window.__ModuleLoader__.load({
|
|
|
5381
5955
|
// is what makes it read centred (the old character needed the same, plus a y nudge for
|
|
5382
5956
|
// its line box, which a path does not have)
|
|
5383
5957
|
var dockBtnEl = iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockTriEl, 12, dockRight ? { x: -1, y: 0 } : { x: 1, y: 0 });
|
|
5384
|
-
// the curtain
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
onClick: openSheet,
|
|
5388
|
-
title: T("sheet.open"),
|
|
5389
|
-
style: {
|
|
5390
|
-
width: 24,
|
|
5391
|
-
height: 24,
|
|
5392
|
-
padding: 0,
|
|
5393
|
-
display: "flex",
|
|
5394
|
-
alignItems: "center",
|
|
5395
|
-
justifyContent: "center",
|
|
5396
|
-
borderRadius: "50%",
|
|
5397
|
-
cornerShape: "round",
|
|
5398
|
-
border: "none",
|
|
5399
|
-
flex: "none",
|
|
5400
|
-
cursor: "pointer",
|
|
5401
|
-
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
5402
|
-
color: C.muted,
|
|
5403
|
-
transition: "background 0.15s ease, color 0.15s ease, opacity 0.2s ease"
|
|
5404
|
-
},
|
|
5405
|
-
onMouseEnter: function (e) {
|
|
5406
|
-
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
5407
|
-
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
5408
|
-
},
|
|
5409
|
-
onMouseLeave: function (e) {
|
|
5410
|
-
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
5411
|
-
e.currentTarget.style.color = C.muted;
|
|
5412
|
-
},
|
|
5413
|
-
children: react_jsx_runtime.jsx("svg", {
|
|
5414
|
-
width: 14,
|
|
5415
|
-
height: 14,
|
|
5416
|
-
viewBox: "0 0 14 14",
|
|
5417
|
-
fill: "none",
|
|
5418
|
-
stroke: "currentColor",
|
|
5419
|
-
strokeWidth: 1.8,
|
|
5420
|
-
strokeLinecap: "round",
|
|
5421
|
-
strokeLinejoin: "round",
|
|
5422
|
-
style: { display: "block", overflow: "visible" },
|
|
5423
|
-
children: [
|
|
5424
|
-
// A landscape surface 12.0 x 9.0 (centred in the 14 box: 1.0..13.0 / 2.5..11.5) —
|
|
5425
|
-
// neither the portrait slab nor the flat strip the earlier attempts were. The
|
|
5426
|
-
// rectangle itself carries the SAME border weight as the questions bubble (2.2,
|
|
5427
|
-
// via its own strokeWidth) while the arrow keeps the lighter inherited 1.8, so the
|
|
5428
|
-
// frame reads as the frame and the arrow stays a mark inside it. `overflow:
|
|
5429
|
-
// visible` because the 2.2 border on a 12-wide rect reaches x = -0.1.
|
|
5430
|
-
react_jsx_runtime.jsx("rect", { x: 1.0, y: 2.5, width: 12.0, height: 9.0, rx: 1.6, strokeWidth: 2.2 }),
|
|
5431
|
-
react_jsx_runtime.jsx("path", { d: "M7 2.5V9.1M5.2 7.3L7 9.1l1.8-1.8" })
|
|
5432
|
-
]
|
|
5433
|
-
})
|
|
5434
|
-
});
|
|
5958
|
+
// the curtain keeps its own entry point: the water-drop handle on the top
|
|
5959
|
+
// edge (sheet.openHandle). The toolbar's curtain button is gone — the header
|
|
5960
|
+
// is four controls now.
|
|
5435
5961
|
// collapse (docked panel only): thick SVG cross, nudged slightly down
|
|
5436
5962
|
var headerCloseBtnEl = react_jsx_runtime.jsx("button", {
|
|
5437
5963
|
onClick: function () { noteClosed("dock"); setOpen(false); },
|
|
@@ -5522,20 +6048,22 @@ window.__ModuleLoader__.load({
|
|
|
5522
6048
|
scrollbarWidth: "none"
|
|
5523
6049
|
},
|
|
5524
6050
|
children: [
|
|
5525
|
-
// LEFT pair:
|
|
5526
|
-
// left end
|
|
5527
|
-
//
|
|
6051
|
+
// LEFT pair: this plugin's settings jump and the dock toggle take
|
|
6052
|
+
// the left end (search + collapse sit at the other end). Four
|
|
6053
|
+
// controls on this bar now: the questions-only toggle moved into
|
|
6054
|
+
// the search row's scope and the curtain button left to the top
|
|
6055
|
+
// handle.
|
|
5528
6056
|
react_jsx_runtime.jsx("div", {
|
|
5529
6057
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
5530
|
-
children: [
|
|
6058
|
+
children: [settingsBtnEl, dockBtnEl]
|
|
5531
6059
|
}),
|
|
5532
6060
|
// spacer: nothing here, it just pins the two pairs to the two ends —
|
|
5533
6061
|
// and it is the first thing to give way when the panel narrows
|
|
5534
6062
|
react_jsx_runtime.jsx("div", { style: { flex: "1 1 auto", minWidth: 0 } }),
|
|
5535
|
-
// RIGHT pair: search +
|
|
6063
|
+
// RIGHT pair: search + collapse
|
|
5536
6064
|
react_jsx_runtime.jsx("div", {
|
|
5537
6065
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
5538
|
-
children: [searchBtnEl,
|
|
6066
|
+
children: [searchBtnEl, headerCloseBtnEl]
|
|
5539
6067
|
})
|
|
5540
6068
|
]
|
|
5541
6069
|
})
|
|
@@ -5543,10 +6071,11 @@ window.__ModuleLoader__.load({
|
|
|
5543
6071
|
});
|
|
5544
6072
|
|
|
5545
6073
|
// ---- curtain chrome: the controls that still apply here (the level filter,
|
|
5546
|
-
//
|
|
6074
|
+
// search) plus the way out. There is no title — the surface is
|
|
5547
6075
|
// self-evident — and no hint text: ✕ and Esc are the obvious exits. The dock /
|
|
5548
6076
|
// curtain / collapse buttons are absent on purpose: there is no docked panel to
|
|
5549
|
-
// move and no second curtain to open.
|
|
6077
|
+
// move and no second curtain to open. The questions-only toggle lives in the
|
|
6078
|
+
// search row's 提问 filter now.
|
|
5550
6079
|
var sheetChromeEl = react_jsx_runtime.jsx("div", {
|
|
5551
6080
|
className: "dqt-sheet-chrome",
|
|
5552
6081
|
title: T("sheet.noDrag"),
|
|
@@ -5561,7 +6090,7 @@ window.__ModuleLoader__.load({
|
|
|
5561
6090
|
children: [
|
|
5562
6091
|
react_jsx_runtime.jsx("div", {
|
|
5563
6092
|
style: { position: "relative", display: "flex", alignItems: "center", gap: 8, flex: "none" },
|
|
5564
|
-
children: [
|
|
6093
|
+
children: [settingsBtnEl]
|
|
5565
6094
|
}),
|
|
5566
6095
|
react_jsx_runtime.jsx("div", { style: { flex: "1 1 auto", minWidth: 0 } }),
|
|
5567
6096
|
searchBtnEl,
|
|
@@ -5572,6 +6101,77 @@ window.__ModuleLoader__.load({
|
|
|
5572
6101
|
// ---- search row: the input plus the scope pill, the fuzzy switch and the
|
|
5573
6102
|
// match counter. In the docked layout it is a row under the header; on the
|
|
5574
6103
|
// curtain it is the head of the search column on the right.
|
|
6104
|
+
// recent searches, offered back while the box is empty: one tap fills the
|
|
6105
|
+
// box (and restores the scope it was searched in); Enter still jumps.
|
|
6106
|
+
// mousedown is swallowed so the input keeps its focus.
|
|
6107
|
+
var searchHistStripEl = (searchOpen && query.trim() === "" && searchHist.length > 0) ? react_jsx_runtime.jsx("div", {
|
|
6108
|
+
"data-dqt-hist": "1",
|
|
6109
|
+
style: {
|
|
6110
|
+
display: "flex",
|
|
6111
|
+
alignItems: "center",
|
|
6112
|
+
gap: 6,
|
|
6113
|
+
// even vertical breathing room on both sides: the text used to hug the
|
|
6114
|
+
// search row above it, and the divider below the strip is the search
|
|
6115
|
+
// block's closing line (NOT a line between the row and its own strip)
|
|
6116
|
+
padding: sheetOpen ? "7px 10px 10px" : "6px 8px 8px",
|
|
6117
|
+
borderBottom: "1px solid " + C.panelBorder,
|
|
6118
|
+
minWidth: 0
|
|
6119
|
+
},
|
|
6120
|
+
children: [
|
|
6121
|
+
react_jsx_runtime.jsx("span", {
|
|
6122
|
+
style: { flex: "none", fontSize: 11, color: C.muted },
|
|
6123
|
+
children: T("search.history")
|
|
6124
|
+
}, "hh-label"),
|
|
6125
|
+
react_jsx_runtime.jsx("div", {
|
|
6126
|
+
style: { flex: "1 1 auto", minWidth: 0, display: "flex", flexWrap: "wrap", gap: 4 },
|
|
6127
|
+
children: searchHist.map(function (entry, hi) {
|
|
6128
|
+
return react_jsx_runtime.jsx("button", {
|
|
6129
|
+
type: "button",
|
|
6130
|
+
"data-dqt-hist-item": hi,
|
|
6131
|
+
title: entry.q,
|
|
6132
|
+
onMouseDown: function (e) { e.preventDefault(); },
|
|
6133
|
+
onClick: function () {
|
|
6134
|
+
if (entry.scope !== searchScope) { setPrevScope(searchScope); setSearchScope(entry.scope); }
|
|
6135
|
+
onQueryChange(entry.q);
|
|
6136
|
+
},
|
|
6137
|
+
style: {
|
|
6138
|
+
flex: "none",
|
|
6139
|
+
maxWidth: 140,
|
|
6140
|
+
overflow: "hidden",
|
|
6141
|
+
textOverflow: "ellipsis",
|
|
6142
|
+
whiteSpace: "nowrap",
|
|
6143
|
+
fontSize: 11,
|
|
6144
|
+
padding: "2px 8px",
|
|
6145
|
+
borderRadius: 999,
|
|
6146
|
+
cornerShape: "round",
|
|
6147
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
6148
|
+
color: C.text,
|
|
6149
|
+
border: "none",
|
|
6150
|
+
cursor: "pointer"
|
|
6151
|
+
},
|
|
6152
|
+
children: entry.q
|
|
6153
|
+
}, "hh-" + hi);
|
|
6154
|
+
})
|
|
6155
|
+
}, "hh-chips"),
|
|
6156
|
+
react_jsx_runtime.jsx("button", {
|
|
6157
|
+
type: "button",
|
|
6158
|
+
"data-dqt-hist-clear": "1",
|
|
6159
|
+
title: T("search.history.clear"),
|
|
6160
|
+
onMouseDown: function (e) { e.preventDefault(); },
|
|
6161
|
+
onClick: function () { setSearchHist(searchHistClear()); },
|
|
6162
|
+
style: {
|
|
6163
|
+
flex: "none",
|
|
6164
|
+
fontSize: 11,
|
|
6165
|
+
padding: "2px 4px",
|
|
6166
|
+
background: "transparent",
|
|
6167
|
+
color: C.muted,
|
|
6168
|
+
border: "none",
|
|
6169
|
+
cursor: "pointer"
|
|
6170
|
+
},
|
|
6171
|
+
children: T("search.history.clear")
|
|
6172
|
+
}, "hh-clear")
|
|
6173
|
+
]
|
|
6174
|
+
}) : null;
|
|
5575
6175
|
var searchRowEl = (searchOpen || searchAnim === "out") ? react_jsx_runtime.jsx("div", {
|
|
5576
6176
|
style: {
|
|
5577
6177
|
display: "grid",
|
|
@@ -5581,13 +6181,15 @@ window.__ModuleLoader__.load({
|
|
|
5581
6181
|
},
|
|
5582
6182
|
children: react_jsx_runtime.jsx("div", {
|
|
5583
6183
|
style: { overflow: "hidden", minHeight: 0 },
|
|
5584
|
-
children: react_jsx_runtime.jsx("div", {
|
|
6184
|
+
children: [react_jsx_runtime.jsx("div", {
|
|
5585
6185
|
style: {
|
|
5586
6186
|
display: "flex",
|
|
5587
6187
|
alignItems: "center",
|
|
5588
6188
|
gap: sheetOpen ? 6 : 4,
|
|
5589
6189
|
padding: sheetOpen ? "4px 10px 8px" : "2px 8px 4px",
|
|
5590
|
-
|
|
6190
|
+
// the closing line belongs to the strip when the history strip is
|
|
6191
|
+
// out: no divider between the search row and its own strip
|
|
6192
|
+
borderBottom: searchHistStripEl ? "none" : ("1px solid " + C.panelBorder),
|
|
5591
6193
|
opacity: (searchAnim === "enter" || searchAnim === "out") ? 0 : 1,
|
|
5592
6194
|
transform: (searchAnim === "enter" || searchAnim === "out") ? "translateY(-6px)" : "none",
|
|
5593
6195
|
transition: "opacity 0.2s ease, transform 0.2s ease"
|
|
@@ -5617,7 +6219,12 @@ window.__ModuleLoader__.load({
|
|
|
5617
6219
|
// (arrows only mean something while a query is typed)
|
|
5618
6220
|
if (e.key === "ArrowDown") { goToMatch(matchIdx + 1); swallow(e); return; }
|
|
5619
6221
|
if (e.key === "ArrowUp") { goToMatch(matchIdx - 1); swallow(e); return; }
|
|
5620
|
-
|
|
6222
|
+
// Enter = the reader committed this search: it joins the
|
|
6223
|
+
// recent-search history even when nothing matched
|
|
6224
|
+
if (e.key === "Enter") {
|
|
6225
|
+
if (query.trim() !== "") commitSearchHist(query.trim(), searchScope);
|
|
6226
|
+
goToMatch(matchIdx + 1); swallow(e); return;
|
|
6227
|
+
}
|
|
5621
6228
|
if (e.key === "Escape") { closeSearch(); swallow(e); }
|
|
5622
6229
|
},
|
|
5623
6230
|
style: {
|
|
@@ -5691,7 +6298,8 @@ window.__ModuleLoader__.load({
|
|
|
5691
6298
|
onClick: toggleScope,
|
|
5692
6299
|
"data-scope": searchScope,
|
|
5693
6300
|
title: searchScope === "title" ? T("search.scope.tipTitle")
|
|
5694
|
-
: (searchScope === "
|
|
6301
|
+
: (searchScope === "prompts" ? T("search.scope.tipPrompts")
|
|
6302
|
+
: (searchScope === "full" ? T("search.scope.tipFull") : T("search.scope.tipCross"))),
|
|
5695
6303
|
style: {
|
|
5696
6304
|
flex: "none",
|
|
5697
6305
|
width: sheetOpen ? 40 : 34,
|
|
@@ -5755,7 +6363,7 @@ window.__ModuleLoader__.load({
|
|
|
5755
6363
|
children: T("search.fuzzy")
|
|
5756
6364
|
})
|
|
5757
6365
|
]
|
|
5758
|
-
})
|
|
6366
|
+
}), searchHistStripEl]
|
|
5759
6367
|
})
|
|
5760
6368
|
}) : null;
|
|
5761
6369
|
|
|
@@ -5888,6 +6496,66 @@ window.__ModuleLoader__.load({
|
|
|
5888
6496
|
style: { flex: "1 1 auto", minHeight: 0, display: "flex", flexDirection: "row", alignItems: "stretch" },
|
|
5889
6497
|
children: [listEl, sheetSearchColEl]
|
|
5890
6498
|
});
|
|
6499
|
+
// one geometry for the panel and its shadow twin, merged (never duplicated)
|
|
6500
|
+
var mergeStyle = function (base, extra) {
|
|
6501
|
+
var out = {};
|
|
6502
|
+
var k;
|
|
6503
|
+
for (k in base) if (Object.prototype.hasOwnProperty.call(base, k)) out[k] = base[k];
|
|
6504
|
+
for (k in extra) if (Object.prototype.hasOwnProperty.call(extra, k)) out[k] = extra[k];
|
|
6505
|
+
return out;
|
|
6506
|
+
};
|
|
6507
|
+
var panelGeoStyle = {
|
|
6508
|
+
position: "fixed",
|
|
6509
|
+
top: zLen(baseTopPx),
|
|
6510
|
+
left: zLen(panelLeft),
|
|
6511
|
+
width: zLen(panelW),
|
|
6512
|
+
maxHeight: zLen(vMaxH),
|
|
6513
|
+
height: panelH > 0 ? zLen(panelH) : undefined,
|
|
6514
|
+
zoom: zoom === 1 ? undefined : zoom,
|
|
6515
|
+
borderRadius: 12,
|
|
6516
|
+
transform: panelTransform,
|
|
6517
|
+
transition: viewFading ? "opacity 0.3s ease" : panelTransition,
|
|
6518
|
+
// held back while the edge handle slips home (see edgeLeaving): the panel is
|
|
6519
|
+
// already at its open geometry here, only the animation waits
|
|
6520
|
+
transitionDelay: edgeLeaving ? EDGE_OUT_MS + "ms" : undefined
|
|
6521
|
+
};
|
|
6522
|
+
// The outer shadow lives on a transparent twin BEHIND the panel. On the panel
|
|
6523
|
+
// itself it could not survive the collapse clip, and the clip's inset must be
|
|
6524
|
+
// the TRAVEL distance for the visible edge to keep its line. The twin is
|
|
6525
|
+
// clipped by the SAME wipe — its cut sitting 8px out (that side's own reach),
|
|
6526
|
+
// so the halo spills exactly as far as it would around the visible shape and
|
|
6527
|
+
// follows that shape through the slide: no shadow left standing past the tuck
|
|
6528
|
+
// line while the panel has gone, and no cut at the edges.
|
|
6529
|
+
//
|
|
6530
|
+
// The three sides the wipe never touches bleed far out instead (24px covers
|
|
6531
|
+
// the deepest reach — 15px below with the offset, more at the bottom corners),
|
|
6532
|
+
// so nothing of the halo is sliced at rest either.
|
|
6533
|
+
//
|
|
6534
|
+
// One halo on ALL four sides — evenly, corners included (a card lifted off
|
|
6535
|
+
// the conversation; no downward bias, or the top corners stay bare). Dark
|
|
6536
|
+
// scheme gets a WHITE one — the theme's own raised language.
|
|
6537
|
+
// the wipe side's cut sits exactly on the seam the panel tucks behind: past
|
|
6538
|
+
// that line there is nothing to see — panel or halo — the clip decides,
|
|
6539
|
+
// never a fade
|
|
6540
|
+
var SHADOW_REACH = 8;
|
|
6541
|
+
var SHADOW_BLEED = 24;
|
|
6542
|
+
var shadowWipe = geoOpen ? 0 : travelPx;
|
|
6543
|
+
var shadowClip = dockRight
|
|
6544
|
+
? "inset(" + zLen(-SHADOW_BLEED) + " " + zLen(shadowWipe - SHADOW_REACH) + " " + zLen(-SHADOW_BLEED) + " " + zLen(-SHADOW_BLEED) + ")"
|
|
6545
|
+
: "inset(" + zLen(-SHADOW_BLEED) + " " + zLen(-SHADOW_BLEED) + " " + zLen(-SHADOW_BLEED) + " " + zLen(shadowWipe - SHADOW_REACH) + ")";
|
|
6546
|
+
var panelShadowEl = react_jsx_runtime.jsx("div", {
|
|
6547
|
+
"data-dqt-panel-shadow": "1",
|
|
6548
|
+
style: mergeStyle(panelGeoStyle, {
|
|
6549
|
+
zIndex: Z_PANEL - 1,
|
|
6550
|
+
background: "transparent",
|
|
6551
|
+
opacity: panelOpacity,
|
|
6552
|
+
pointerEvents: "none",
|
|
6553
|
+
clipPath: shadowClip,
|
|
6554
|
+
boxShadow: isDarkScheme()
|
|
6555
|
+
? "0 0 24px -4px rgba(255, 255, 255, 0.16), 0 0 10px -2px rgba(255, 255, 255, 0.10)"
|
|
6556
|
+
: "0 0 24px -4px rgba(0, 0, 0, 0.38), 0 0 10px -2px rgba(0, 0, 0, 0.26)"
|
|
6557
|
+
})
|
|
6558
|
+
});
|
|
5891
6559
|
var panelEl = react_jsx_runtime.jsx("div", {
|
|
5892
6560
|
ref: panelRootRef,
|
|
5893
6561
|
// the panel takes focus on any click inside it (except into a text field), so
|
|
@@ -5922,37 +6590,18 @@ window.__ModuleLoader__.load({
|
|
|
5922
6590
|
// the curtain's own scale (the docked panel's does not apply here)
|
|
5923
6591
|
zoom: sheetZoom === 1 ? undefined : sheetZoom,
|
|
5924
6592
|
pointerEvents: chatViewActive ? "auto" : "none"
|
|
5925
|
-
} : {
|
|
5926
|
-
position: "fixed",
|
|
5927
|
-
top: zLen(baseTopPx),
|
|
5928
|
-
left: zLen(panelLeft),
|
|
5929
|
-
width: zLen(panelW),
|
|
5930
|
-
maxHeight: zLen(vMaxH),
|
|
5931
|
-
height: panelH > 0 ? zLen(panelH) : undefined,
|
|
5932
|
-
zoom: zoom === 1 ? undefined : zoom,
|
|
6593
|
+
} : mergeStyle(panelGeoStyle, {
|
|
5933
6594
|
display: "flex",
|
|
5934
6595
|
flexDirection: "column",
|
|
5935
6596
|
background: C.panelBg,
|
|
5936
6597
|
border: "none",
|
|
5937
|
-
borderRadius: 12,
|
|
5938
6598
|
zIndex: Z_PANEL,
|
|
5939
6599
|
overflow: "hidden",
|
|
5940
6600
|
color: C.text,
|
|
5941
6601
|
opacity: panelOpacity,
|
|
5942
6602
|
clipPath: panelClip,
|
|
5943
|
-
transform: panelTransform,
|
|
5944
|
-
// inner shadow only (no outer): theme-aware (white bevel in dark mode,
|
|
5945
|
-
// soft shade in light) — outer shadow would be clipped by the collapse
|
|
5946
|
-
// clip-path and the panel edges.
|
|
5947
|
-
boxShadow: innerShadow(),
|
|
5948
|
-
// A view switch swaps in a short, delay-free opacity fade; the dock and
|
|
5949
|
-
// collapse animations keep their own (slower) timings.
|
|
5950
|
-
transition: viewFading ? "opacity 0.3s ease" : panelTransition,
|
|
5951
|
-
// held back while the edge handle slips home (see edgeLeaving): the panel is
|
|
5952
|
-
// already at its open geometry here, only the animation waits
|
|
5953
|
-
transitionDelay: edgeLeaving ? EDGE_OUT_MS + "ms" : undefined,
|
|
5954
6603
|
pointerEvents: chatViewActive ? "auto" : "none"
|
|
5955
|
-
},
|
|
6604
|
+
}),
|
|
5956
6605
|
onMouseEnter: function () { setHovered(true); },
|
|
5957
6606
|
onMouseLeave: function () { setHovered(false); },
|
|
5958
6607
|
onClickCapture: function (e) {
|
|
@@ -6180,8 +6829,7 @@ window.__ModuleLoader__.load({
|
|
|
6180
6829
|
},
|
|
6181
6830
|
onPointerDown: onResizeCornerDown,
|
|
6182
6831
|
title: T("resize.wh")
|
|
6183
|
-
})
|
|
6184
|
-
levelsPopDockEl
|
|
6832
|
+
})
|
|
6185
6833
|
]
|
|
6186
6834
|
});
|
|
6187
6835
|
|
|
@@ -6285,6 +6933,14 @@ window.__ModuleLoader__.load({
|
|
|
6285
6933
|
var handleH = sheetStrip ? Math.max(16, Math.min(HANDLE_BOX_MAX, stripH)) : HANDLE_BOX_MAX;
|
|
6286
6934
|
var handleTopPx = sheetStrip ? sheetStrip.bottom - handleH : sheetTopPx;
|
|
6287
6935
|
|
|
6936
|
+
// The curtain runs edge to edge of its area: the conversation column sits a
|
|
6937
|
+
// couple of pixels inside the window (the host's own outer padding), and that
|
|
6938
|
+
// sliver used to read as a gap beside the curtain. An inset this small IS the
|
|
6939
|
+
// padding — it bleeds under the curtain on either side; a real column (the
|
|
6940
|
+
// sidebar, the docked panel) stays respected.
|
|
6941
|
+
var SHEET_EDGE_PAD_MAX = 12;
|
|
6942
|
+
var sheetInset = function (px) { return px <= SHEET_EDGE_PAD_MAX ? 0 : px; };
|
|
6943
|
+
|
|
6288
6944
|
// ---- curtain handle resting on the view tab strip's line -----------------
|
|
6289
6945
|
// A droplet held to the line by surface tension rather than a floating pill: the
|
|
6290
6946
|
// silhouette flares out into two concave fillets that meet the strip's lower line
|
|
@@ -6323,7 +6979,7 @@ window.__ModuleLoader__.load({
|
|
|
6323
6979
|
// if the stored area rect is one layout generation behind
|
|
6324
6980
|
left: Math.max(8, Math.min(Math.max(8, window.innerWidth - DROP_W - 8),
|
|
6325
6981
|
viewport
|
|
6326
|
-
? viewport.left + (Math.max(320, window.innerWidth - viewport.left - viewport.right) - DROP_W) / 2
|
|
6982
|
+
? sheetInset(viewport.left) + (Math.max(320, window.innerWidth - sheetInset(viewport.left) - sheetInset(viewport.right)) - DROP_W) / 2
|
|
6327
6983
|
: (window.innerWidth - DROP_W) / 2)) + "px"
|
|
6328
6984
|
},
|
|
6329
6985
|
title: T("sheet.openHandle"),
|
|
@@ -6610,6 +7266,9 @@ window.__ModuleLoader__.load({
|
|
|
6610
7266
|
// The outer box never unmounts: while the curtain is down it is a zero-size,
|
|
6611
7267
|
// non-clipping pass-through, so the panel (and the whole list DOM) is never
|
|
6612
7268
|
// moved or rebuilt when the curtain opens or closes.
|
|
7269
|
+
// How far the clip box extends BELOW the curtain: that is where the curtain's
|
|
7270
|
+
// bottom shadow lives (the clip would otherwise cut it off).
|
|
7271
|
+
var SHEET_SHADOW_ROOM = 24;
|
|
6613
7272
|
var sheetUp = sheetOpen && chatViewActive;
|
|
6614
7273
|
var sheetShellEl = react_jsx_runtime.jsx("div", {
|
|
6615
7274
|
key: "sheet-shell",
|
|
@@ -6619,15 +7278,16 @@ window.__ModuleLoader__.load({
|
|
|
6619
7278
|
position: "fixed",
|
|
6620
7279
|
// hangs from just below the 对话/轨迹/上下文 strip
|
|
6621
7280
|
top: sheetTopPx,
|
|
6622
|
-
left: viewport ? viewport.left : 0,
|
|
6623
|
-
width: viewport ? Math.max(320, window.innerWidth - viewport.left - viewport.right) : "100%",
|
|
7281
|
+
left: viewport ? sheetInset(viewport.left) : 0,
|
|
7282
|
+
width: viewport ? Math.max(320, window.innerWidth - sheetInset(viewport.left) - sheetInset(viewport.right)) : "100%",
|
|
6624
7283
|
// a curtain over the conversation, not the whole screen: the composer and
|
|
6625
7284
|
// the newest message stay reachable below it. The height is measured from
|
|
6626
7285
|
// the conversation box (not from the curtain's own top) so hanging it below
|
|
6627
|
-
// the tabs does not push its bottom edge down over the composer.
|
|
7286
|
+
// the tabs does not push its bottom edge down over the composer. The clip
|
|
7287
|
+
// then grows by the shadow room below the curtain itself.
|
|
6628
7288
|
height: viewport
|
|
6629
|
-
? Math.max(240, Math.min(760, (viewport.height - Math.max(0, sheetTopPx - viewport.top)) * 0.82))
|
|
6630
|
-
: "70vh",
|
|
7289
|
+
? Math.max(240, Math.min(760, (viewport.height - Math.max(0, sheetTopPx - viewport.top)) * 0.82)) + SHEET_SHADOW_ROOM
|
|
7290
|
+
: "calc(70vh + " + SHEET_SHADOW_ROOM + "px)",
|
|
6631
7291
|
overflow: "hidden",
|
|
6632
7292
|
zIndex: Z_SHEET
|
|
6633
7293
|
} : {
|
|
@@ -6666,19 +7326,30 @@ window.__ModuleLoader__.load({
|
|
|
6666
7326
|
"data-sheet": sheetUp ? "on" : undefined,
|
|
6667
7327
|
style: {
|
|
6668
7328
|
width: sheetUp ? "100%" : 0,
|
|
6669
|
-
|
|
7329
|
+
// the bottom leaves the shadow room the clip box grew by
|
|
7330
|
+
height: sheetUp ? "calc(100% - " + SHEET_SHADOW_ROOM + "px)" : 0,
|
|
7331
|
+
// border-box so the 1px edges stay INSIDE the measured height: with the
|
|
7332
|
+
// old content-box the bottom pair sat below it and the clip cut them
|
|
7333
|
+
// away, which is why the bottom edge had no visible line
|
|
7334
|
+
boxSizing: "border-box",
|
|
6670
7335
|
display: "flex",
|
|
6671
7336
|
flexDirection: "column",
|
|
6672
7337
|
background: C.panelBg,
|
|
6673
7338
|
color: C.text,
|
|
6674
|
-
border:
|
|
7339
|
+
border: "none",
|
|
7340
|
+
borderTop: sheetUp ? "1px solid " + C.panelBorder : "none",
|
|
7341
|
+
borderLeft: sheetUp ? "1px solid " + C.panelBorder : "none",
|
|
7342
|
+
borderRight: sheetUp ? "1px solid " + C.panelBorder : "none",
|
|
7343
|
+
borderBottom: sheetUp ? "1px solid " + C.sheetEdge : "none",
|
|
6675
7344
|
// square where it meets the top edge, since the curtain hangs from there;
|
|
6676
|
-
// no dimming layer
|
|
6677
|
-
//
|
|
7345
|
+
// no dimming layer — the conversation below stays legible and usable.
|
|
7346
|
+
// The outer shadow is the bottom edge's own: it follows the rounded
|
|
7347
|
+
// bottom corners and stops before the vertical seams.
|
|
6678
7348
|
borderRadius: "0 0 12px 12px",
|
|
7349
|
+
boxShadow: sheetUp ? C.sheetShadow : "none",
|
|
6679
7350
|
overflow: "hidden"
|
|
6680
7351
|
},
|
|
6681
|
-
children: panelEl
|
|
7352
|
+
children: sheetOpen ? panelEl : [panelShadowEl, panelEl]
|
|
6682
7353
|
})
|
|
6683
7354
|
});
|
|
6684
7355
|
|
|
@@ -6695,13 +7366,18 @@ window.__ModuleLoader__.load({
|
|
|
6695
7366
|
// stays hidden until the row is hovered — the row's own mouseenter/leave
|
|
6696
7367
|
// reveal it — and the click is handled once, in the panel's capture phase,
|
|
6697
7368
|
// so the row's jump-to-start can never fire alongside it.
|
|
6698
|
-
function renderEndBtn(C, key, idx, turn) {
|
|
7369
|
+
function renderEndBtn(C, key, idx, turn, small) {
|
|
6699
7370
|
// These markers are not decoration: the row's hover handler looks for
|
|
6700
7371
|
// [data-dqt-end] to reveal the control, and the panel's capture phase looks
|
|
6701
7372
|
// for it to route the click to the end jump instead of the row's own
|
|
6702
7373
|
// jump-to-start. They are deliberately NOT data-jump-key: that attribute
|
|
6703
7374
|
// belongs to the rows themselves, and two kinds of element claiming it made
|
|
6704
7375
|
// every "find the row" query ambiguous.
|
|
7376
|
+
// `small` = compact density: the tight rows are 16px tall and clip their
|
|
7377
|
+
// contents, so a 20px circle would have its backdrop cut top and bottom —
|
|
7378
|
+
// the control scales with the row it lives in.
|
|
7379
|
+
var size = small ? 16 : 20;
|
|
7380
|
+
var icon = small ? 10 : 12;
|
|
6705
7381
|
var attrs = { "data-dqt-end": "1" };
|
|
6706
7382
|
if (key) attrs["data-end-key"] = key;
|
|
6707
7383
|
if (idx !== undefined && idx !== null) attrs["data-end-idx"] = String(idx);
|
|
@@ -6723,8 +7399,8 @@ window.__ModuleLoader__.load({
|
|
|
6723
7399
|
right: 4,
|
|
6724
7400
|
top: "50%",
|
|
6725
7401
|
transform: "translateY(-50%)",
|
|
6726
|
-
width:
|
|
6727
|
-
height:
|
|
7402
|
+
width: size,
|
|
7403
|
+
height: size,
|
|
6728
7404
|
display: "flex",
|
|
6729
7405
|
alignItems: "center",
|
|
6730
7406
|
justifyContent: "center",
|
|
@@ -6741,8 +7417,8 @@ window.__ModuleLoader__.load({
|
|
|
6741
7417
|
transition: "opacity 0.22s ease, background 0.18s ease, color 0.18s ease"
|
|
6742
7418
|
},
|
|
6743
7419
|
children: react_jsx_runtime.jsx("svg", {
|
|
6744
|
-
width:
|
|
6745
|
-
height:
|
|
7420
|
+
width: icon,
|
|
7421
|
+
height: icon,
|
|
6746
7422
|
viewBox: "0 0 12 12",
|
|
6747
7423
|
style: { display: "block" },
|
|
6748
7424
|
children: react_jsx_runtime.jsx("path", {
|
|
@@ -6765,6 +7441,7 @@ window.__ModuleLoader__.load({
|
|
|
6765
7441
|
// the title, and the section's opening text out to the right — the space the
|
|
6766
7442
|
// docked panel does not have. The narrow panel keeps title-over-subtitle.
|
|
6767
7443
|
var wide = !!(extra && extra.wide);
|
|
7444
|
+
var dense = !!(extra && extra.dense);
|
|
6768
7445
|
var hoverInfo = {
|
|
6769
7446
|
title: n.title,
|
|
6770
7447
|
sub: n.sub || "",
|
|
@@ -6821,8 +7498,8 @@ window.__ModuleLoader__.load({
|
|
|
6821
7498
|
if (n.sub) {
|
|
6822
7499
|
rows.push(react_jsx_runtime.jsx("div", {
|
|
6823
7500
|
style: {
|
|
6824
|
-
fontSize: 10,
|
|
6825
|
-
lineHeight: "13px",
|
|
7501
|
+
fontSize: dense ? 9.5 : 10,
|
|
7502
|
+
lineHeight: dense ? "11px" : "13px",
|
|
6826
7503
|
color: C.muted,
|
|
6827
7504
|
opacity: 0.75,
|
|
6828
7505
|
whiteSpace: "nowrap",
|
|
@@ -6836,7 +7513,7 @@ window.__ModuleLoader__.load({
|
|
|
6836
7513
|
// the section's own "jump to its end" control, revealed while the row is
|
|
6837
7514
|
// hovered; the reserved right padding keeps the title/subtitle/context
|
|
6838
7515
|
// clear of it so nothing ever runs underneath the button
|
|
6839
|
-
rows.push(renderEndBtn(C, n.key, n.idx));
|
|
7516
|
+
rows.push(renderEndBtn(C, n.key, n.idx, undefined, dense && !wide));
|
|
6840
7517
|
// The hover highlight is its own layer rather than the row's own background:
|
|
6841
7518
|
// the row box runs to its right edge, which is exactly where the end control's
|
|
6842
7519
|
// round backdrop appears, so the two boxes used to sit flush against each other
|
|
@@ -6886,16 +7563,16 @@ window.__ModuleLoader__.load({
|
|
|
6886
7563
|
// (narrow layout) — in wide mode it is one line by construction
|
|
6887
7564
|
display: "block",
|
|
6888
7565
|
position: "relative",
|
|
6889
|
-
padding: wide ? "4px 8px" : "2px 6px",
|
|
7566
|
+
padding: dense ? (wide ? "1px 8px" : "0px 6px") : wide ? "4px 8px" : "2px 6px",
|
|
6890
7567
|
paddingRight: END_ZONE,
|
|
6891
7568
|
paddingLeft: (hasChildren ? (wide ? 4 : 2) : (wide ? 8 : 6)) + (n.level - 1) * (wide ? 16 : 12),
|
|
6892
|
-
margin: "1px 0",
|
|
7569
|
+
margin: dense ? "0 0" : "1px 0",
|
|
6893
7570
|
borderRadius: 6,
|
|
6894
7571
|
cursor: "pointer",
|
|
6895
7572
|
fontSize: n.level <= 2 ? (wide ? 16.5 : 13) : (wide ? 15.5 : 12),
|
|
6896
7573
|
color: n.level <= 2 ? C.text : C.muted,
|
|
6897
7574
|
fontWeight: n.level <= 2 ? 600 : 400,
|
|
6898
|
-
lineHeight: wide ? "26px" : "18px",
|
|
7575
|
+
lineHeight: dense ? (wide ? "23px" : "16px") : wide ? "26px" : "18px",
|
|
6899
7576
|
whiteSpace: "nowrap",
|
|
6900
7577
|
overflow: "hidden"
|
|
6901
7578
|
},
|
|
@@ -7150,6 +7827,8 @@ window.__ModuleLoader__.load({
|
|
|
7150
7827
|
// either way, and every header in a given view still shares ONE style.
|
|
7151
7828
|
var big = !!(extra && (extra.questionsOnly || extra.wide));
|
|
7152
7829
|
var wideBand = !!(extra && extra.wide && !extra.questionsOnly);
|
|
7830
|
+
// compact density: the same one-line header, a couple of notches tighter
|
|
7831
|
+
var dense = !!(extra && extra.dense);
|
|
7153
7832
|
// a turn that ended in a failure has no reply to land on: its terminal row IS
|
|
7154
7833
|
// the interesting position, so the header lands there (and says so in the tip)
|
|
7155
7834
|
var replyKey = (g.msgs && g.msgs.length > 0) ? g.msgs[0].key : (g.failure ? g.failure.key : "");
|
|
@@ -7179,14 +7858,15 @@ window.__ModuleLoader__.load({
|
|
|
7179
7858
|
// headings: same box, same 18px rhythm, same left inset, so the time
|
|
7180
7859
|
// column of a heading-less turn lines up with all the others. Only the
|
|
7181
7860
|
// type emphasis differs (weight + colour, see the label below).
|
|
7182
|
-
padding: big ? (wideBand ? "
|
|
7861
|
+
padding: dense ? (big ? (wideBand ? "1px 4px 2px" : "0px 4px 1px") : "0px 4px 1px")
|
|
7862
|
+
: big ? (wideBand ? "3px 4px 4px" : "2px 4px 3px") : "1px 4px 2px",
|
|
7183
7863
|
// the label is capped by this, not by a maxWidth guess: the row reserves the
|
|
7184
7864
|
// end control's zone, so a long prompt's hover box ends END_ZONE from the row
|
|
7185
7865
|
// edge — a real gap before the button's round backdrop, which sits 24px in.
|
|
7186
7866
|
// (With `max-width: calc(100% - 32px)` the percentage resolved somewhere else
|
|
7187
7867
|
// and a forced-long header still came within 2px of the circle.)
|
|
7188
7868
|
paddingRight: END_ZONE,
|
|
7189
|
-
height: big ? (wideBand ? 24 : 22) : 18,
|
|
7869
|
+
height: dense ? (big ? (wideBand ? 21 : 19) : 15) : big ? (wideBand ? 24 : 22) : 18,
|
|
7190
7870
|
display: "flex",
|
|
7191
7871
|
alignItems: "center",
|
|
7192
7872
|
minWidth: 0,
|
|
@@ -7229,6 +7909,12 @@ window.__ModuleLoader__.load({
|
|
|
7229
7909
|
// on the group box — never on the label.
|
|
7230
7910
|
fontSize: big ? (wideBand ? 14 : 12.5) : 11,
|
|
7231
7911
|
fontWeight: 400,
|
|
7912
|
+
// Compact density fixes the header's HEIGHT, so the label's line box
|
|
7913
|
+
// must fit inside it: the inherited line-height left the label taller
|
|
7914
|
+
// than its own row, and every background riding on it (the current
|
|
7915
|
+
// group's tint, the hover grey, the keyboard cursor) was cut to the
|
|
7916
|
+
// shorter band. Comfortable density keeps the inherited rhythm.
|
|
7917
|
+
lineHeight: dense ? (big ? (wideBand ? "17px" : "14px") : "13px") : undefined,
|
|
7232
7918
|
color: big ? C.text : C.muted,
|
|
7233
7919
|
cursor: "pointer",
|
|
7234
7920
|
padding: big ? "1px 5px" : "1px 5px",
|
|
@@ -7259,7 +7945,7 @@ window.__ModuleLoader__.load({
|
|
|
7259
7945
|
}) : null
|
|
7260
7946
|
]
|
|
7261
7947
|
}),
|
|
7262
|
-
renderEndBtn(C, lastKey, undefined, g.turn)
|
|
7948
|
+
renderEndBtn(C, lastKey, undefined, g.turn, dense && !wideBand)
|
|
7263
7949
|
]
|
|
7264
7950
|
}, "g-h-" + gi);
|
|
7265
7951
|
}
|
|
@@ -7316,8 +8002,8 @@ window.__ModuleLoader__.load({
|
|
|
7316
8002
|
"data-nav-row": "1",
|
|
7317
8003
|
title: T("turn.loadTip"),
|
|
7318
8004
|
style: {
|
|
7319
|
-
padding: "3px 6px",
|
|
7320
|
-
margin: "1px 0",
|
|
8005
|
+
padding: extra.dense ? "1px 6px" : "3px 6px",
|
|
8006
|
+
margin: extra.dense ? "0 0" : "1px 0",
|
|
7321
8007
|
borderRadius: 6,
|
|
7322
8008
|
cursor: "pointer",
|
|
7323
8009
|
minWidth: 0,
|
|
@@ -7332,8 +8018,9 @@ window.__ModuleLoader__.load({
|
|
|
7332
8018
|
// outline lists it as a normal turn and adds this row, so a turn whose request
|
|
7333
8019
|
// never came back reads as "请求失败 · <provider message>" instead of "未加载"
|
|
7334
8020
|
// and never silently disappears from the outline.
|
|
7335
|
-
function renderFailureRow(g, gi, jump, C) {
|
|
8021
|
+
function renderFailureRow(g, gi, jump, C, extra) {
|
|
7336
8022
|
var f = g.failure;
|
|
8023
|
+
var dense = !!(extra && extra.dense);
|
|
7337
8024
|
var message = f.message || f.code || "";
|
|
7338
8025
|
var open = function (e) {
|
|
7339
8026
|
e.stopPropagation();
|
|
@@ -7349,8 +8036,8 @@ window.__ModuleLoader__.load({
|
|
|
7349
8036
|
alignItems: "center",
|
|
7350
8037
|
gap: 5,
|
|
7351
8038
|
minWidth: 0,
|
|
7352
|
-
padding: "1px 6px",
|
|
7353
|
-
margin: "1px 0",
|
|
8039
|
+
padding: dense ? "0px 6px" : "1px 6px",
|
|
8040
|
+
margin: dense ? "0 0" : "1px 0",
|
|
7354
8041
|
borderRadius: 6,
|
|
7355
8042
|
cursor: "pointer",
|
|
7356
8043
|
transition: "background 0.15s ease"
|
|
@@ -7383,6 +8070,7 @@ window.__ModuleLoader__.load({
|
|
|
7383
8070
|
function renderGroups(groups, trees, jump, C, offset, activeIdx, extra) {
|
|
7384
8071
|
extra = extra || {};
|
|
7385
8072
|
var base = offset || 0;
|
|
8073
|
+
var dense = !!extra.dense;
|
|
7386
8074
|
var activeSet = {};
|
|
7387
8075
|
if (Array.isArray(activeIdx)) {
|
|
7388
8076
|
for (var a = 0; a < activeIdx.length; a++) activeSet[activeIdx[a]] = true;
|
|
@@ -7396,7 +8084,7 @@ window.__ModuleLoader__.load({
|
|
|
7396
8084
|
// The divider between two groups stays OUTSIDE the highlight box: inside it
|
|
7397
8085
|
// would draw a rule straight across the tinted block.
|
|
7398
8086
|
out.push(react_jsx_runtime.jsx("div", {
|
|
7399
|
-
style: { borderTop: "1px solid " + C.panelBorder, margin: "7px 2px 3px", height: 0 }
|
|
8087
|
+
style: { borderTop: "1px solid " + C.panelBorder, margin: dense ? "3px 2px 1px" : "7px 2px 3px", height: 0 }
|
|
7400
8088
|
}, "g-sep-" + gi));
|
|
7401
8089
|
var items = [];
|
|
7402
8090
|
if (g.ghost) {
|
|
@@ -7410,7 +8098,7 @@ window.__ModuleLoader__.load({
|
|
|
7410
8098
|
// entirely (the header itself grows and brightens for that job)
|
|
7411
8099
|
if (!extra.questionsOnly) items.push(renderNodes(rows, 0, jump, C, "g" + gi, extra));
|
|
7412
8100
|
// the failure is the turn's LAST event, so its row sits under the rows
|
|
7413
|
-
if (g.failure) items.push(renderFailureRow(g, gi, jump, C));
|
|
8101
|
+
if (g.failure) items.push(renderFailureRow(g, gi, jump, C, extra));
|
|
7414
8102
|
}
|
|
7415
8103
|
// The group being read stays at full opacity and every other group is dimmed
|
|
7416
8104
|
// to 0.85 — enough to mark the reading position without turning the list grey
|
|
@@ -7434,8 +8122,19 @@ window.__ModuleLoader__.load({
|
|
|
7434
8122
|
borderBottom: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
7435
8123
|
borderLeft: "3px solid " + (isActive ? C.groupEdge : "transparent"),
|
|
7436
8124
|
borderRadius: 6,
|
|
7437
|
-
margin: "2px -5px",
|
|
7438
|
-
paddingLeft: 2
|
|
8125
|
+
margin: dense ? "0 -5px" : "2px -5px",
|
|
8126
|
+
paddingLeft: 2,
|
|
8127
|
+
// Long-session virtualization: every offscreen group is skipped by the
|
|
8128
|
+
// renderer (no layout, no paint) until it nears the viewport, while its
|
|
8129
|
+
// DOM stays in place — so the row queries the follow, the keyboard
|
|
8130
|
+
// cursor, the hover card and the jump targets run against are untouched.
|
|
8131
|
+
// `auto` keeps each group's real height after its first render; the
|
|
8132
|
+
// estimate below is only what a never-seen group stands in with.
|
|
8133
|
+
// While a surface is ANIMATING the hold lifts the skip entirely:
|
|
8134
|
+
// a compositor-driven slide does not re-evaluate the skip state, so
|
|
8135
|
+
// an opening surface would otherwise stay blank until it lands.
|
|
8136
|
+
contentVisibility: extra && extra.virtHold ? "visible" : "auto",
|
|
8137
|
+
containIntrinsicSize: "auto " + (dense ? 80 : 96) + "px"
|
|
7439
8138
|
},
|
|
7440
8139
|
children: items
|
|
7441
8140
|
}, "g-" + gi));
|
|
@@ -7638,6 +8337,9 @@ window.__ModuleLoader__.load({
|
|
|
7638
8337
|
seg([["left", T("settings.dock.left")], ["right", T("settings.dock.right")]], prefs.dock,
|
|
7639
8338
|
function (v) { commit({ dock: v }); }), true),
|
|
7640
8339
|
field("levels", T("settings.levels"), "", levelsControl(true), true),
|
|
8340
|
+
field("density", T("settings.density"), "",
|
|
8341
|
+
seg([["comfy", T("settings.density.comfy")], ["compact", T("settings.density.compact")]], prefs.density,
|
|
8342
|
+
function (v) { commit({ density: v }); }), true),
|
|
7641
8343
|
field("zoom", T("settings.zoom"), T("settings.zoom.tip"), zoomControl, false),
|
|
7642
8344
|
field("sheetZoom", T("settings.sheetZoom"), T("settings.sheetZoom.tip"), sheetZoomControl, false),
|
|
7643
8345
|
field("handle", T("settings.handle"), T("settings.handle.tip"), handleControl, false),
|
|
@@ -7707,6 +8409,8 @@ window.__ModuleLoader__.load({
|
|
|
7707
8409
|
var inject = ["slots", "locale"];
|
|
7708
8410
|
|
|
7709
8411
|
function apply(ctx) {
|
|
8412
|
+
// the settings button's navigation looks host services up through this
|
|
8413
|
+
settingsHostCtx = ctx;
|
|
7710
8414
|
ctx.effect(function () {
|
|
7711
8415
|
return ctx.locale.register("dsh-quick-toc", { zh: zh, en: en });
|
|
7712
8416
|
}, "dsh-quick-toc: dictionaries");
|