dsh-quick-toc 0.6.3 → 0.7.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 +17 -0
- package/CHANGELOG.md +17 -0
- package/README.en.md +20 -7
- package/README.md +20 -7
- package/lib/client.js +2482 -536
- package/lib/index.js +4 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -13,13 +13,65 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
|
|
14
14
|
// ---------- constants ----------
|
|
15
15
|
var PANEL_WIDTH = 288;
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
16
|
+
// ---------- layers ----------
|
|
17
|
+
// This plugin floats over the conversation, so it belongs in the ONE free band
|
|
18
|
+
// of the stacking order: above the app's own content, below everything that
|
|
19
|
+
// claims to be an overlay. Outranking other overlays is not a neutral choice —
|
|
20
|
+
// the previous flat z 500 painted the handles and the panel over other plugins'
|
|
21
|
+
// dialogs, and anything sitting on a dialog's close button makes that dialog
|
|
22
|
+
// impossible to dismiss.
|
|
23
|
+
//
|
|
24
|
+
// Measured neighbours (dsh-web-frontend/dist CSS plus every @deepseek-ai
|
|
25
|
+
// dsh-client-ui-* bundle and the installed plugins — values, not guesses):
|
|
26
|
+
// 0-6 trajectory view internals
|
|
27
|
+
// 7-10 conversation: sticky composer seat 7, transcript width grips 8
|
|
28
|
+
// ([data-width-handle], width cap 40px, visible only on hover),
|
|
29
|
+
// composer with a trigger menu 9, workspace row 10
|
|
30
|
+
// 11 layout frame
|
|
31
|
+
// 20 layout overlay layer (pointer-events:none, holds floating chrome)
|
|
32
|
+
// 25 dsh-better-sidebar panel host (its panels live inside it)
|
|
33
|
+
// 30 dsh-client-ui-cordis panel
|
|
34
|
+
// 40/60 right sidebar fullscreen panel / float host
|
|
35
|
+
// 100 menus and popovers (six packages)
|
|
36
|
+
// 200 dsh-context modal mask + card (.lc-modal-backdrop, .lc-ov-backdrop)
|
|
37
|
+
// 1000 DSH modals (settings, image viewer)
|
|
38
|
+
// 1100 tooltips and the app's top overlays
|
|
39
|
+
// So the whole band from 12 to 18 is free, and it is exactly where a reading
|
|
40
|
+
// aid belongs: above the transcript (including the width grips, which would
|
|
41
|
+
// otherwise take the pointer over our left edge) and below everything that is
|
|
42
|
+
// an overlay, the app's own floating chrome included.
|
|
43
|
+
var Z_HANDLE = 12; // the two edge handles
|
|
44
|
+
var Z_PANEL = 14; // the docked panel
|
|
45
|
+
var Z_SHEET = 16; // the curtain, which replaces the docked panel while it is up
|
|
46
|
+
var Z_HOVER = 18; // hover preview card: above both, still inside the free band
|
|
47
|
+
var Z_ERROR = 32; // crash banner: above our layers and the app's chrome, below
|
|
48
|
+
// DSH's popovers (100) and modals (1000)
|
|
22
49
|
var EASE = "cubic-bezier(0.22, 0.9, 0.3, 1)"; // smooth non-linear slide
|
|
50
|
+
// How long the docked panel takes to get out of the curtain's way. It is the
|
|
51
|
+
// panel's own collapse duration (0.28s) plus a hair, so the curtain's drop only
|
|
52
|
+
// starts once the panel has actually finished travelling.
|
|
53
|
+
var PANEL_TUCK_MS = 300;
|
|
54
|
+
// The row-end control ("jump to the end of this section") lives in the last 24px
|
|
55
|
+
// of a row (a 20px button inset 4px). Every box a row can paint — the hover
|
|
56
|
+
// highlight, a group header's hover label, the right-aligned context text — stops
|
|
57
|
+
// END_ZONE from the row's right edge, so such a box can never touch the round
|
|
58
|
+
// backdrop the button paints on hover, let alone overlap it: the reader saw the
|
|
59
|
+
// two "fight" on long group headers, the label's box running right up to the
|
|
60
|
+
// circle.
|
|
61
|
+
var END_ZONE = 32;
|
|
62
|
+
// How long the collapsed edge handle takes to slip back into the edge when it is
|
|
63
|
+
// the thing that was clicked. The panel's own slide waits exactly this long, so the
|
|
64
|
+
// two motions are sequential instead of crossing over.
|
|
65
|
+
var EDGE_OUT_MS = 170;
|
|
66
|
+
// Same idea for the curtain handle: it is pressed flat and held for a beat before it
|
|
67
|
+
// leaves the tree, otherwise the flatten had no frame to play on (the curtain used
|
|
68
|
+
// to open and unmount it in the same commit). It covers the whole 200ms press — the
|
|
69
|
+
// press is deliberately slow-then-fast (see the element), so the curtain must not
|
|
70
|
+
// start before it has finished.
|
|
71
|
+
var DROP_OUT_MS = 220;
|
|
72
|
+
// Width of the curtain's search column: it is pushed in from the right edge and
|
|
73
|
+
// pushes the outline left by exactly this much while a search is up.
|
|
74
|
+
var SHEET_SEARCH_W = 360;
|
|
23
75
|
|
|
24
76
|
// ---------- UI strings ----------
|
|
25
77
|
// Every string the panel renders goes through T(). 0.5.0 ships the Chinese
|
|
@@ -35,16 +87,41 @@ window.__ModuleLoader__.load({
|
|
|
35
87
|
"handle.dockRight": "移到右侧",
|
|
36
88
|
"handle.collapse": "收起",
|
|
37
89
|
"handle.expand": "展开大纲",
|
|
90
|
+
"a11y.list": "大纲列表:上下键移动,回车跳转,Esc 收起面板",
|
|
91
|
+
"a11y.hits": "第 {n} 个命中,共 {total} 个",
|
|
92
|
+
"a11y.results": "共 {n} 条结果",
|
|
93
|
+
"a11y.loading": "正在检索",
|
|
94
|
+
"a11y.crossEmpty": "没有结果",
|
|
95
|
+
"sheet.open": "全屏大纲(从顶部展开一层幕布)",
|
|
96
|
+
"sheet.openHandle": "展开幕布:全宽大纲从顶部落下",
|
|
97
|
+
"sheet.close": "收起幕布",
|
|
98
|
+
"sheet.noDrag": "幕布铺满对话区,不能拖动尺寸或位置",
|
|
38
99
|
"levels.tip": "标题层级筛选",
|
|
39
100
|
"levels.show": "显示 H",
|
|
40
101
|
"levels.hide": "隐藏 H",
|
|
102
|
+
"levels.offInQuestions": "只看提问时没有标题行,层级筛选不生效",
|
|
103
|
+
"view.questions.tipOn": "当前:只看提问(每轮只显示时间与你的提问)。点击回到完整大纲",
|
|
104
|
+
"view.questions.tipOff": "当前:完整大纲。点击只看提问(每轮只显示时间与你的提问)",
|
|
41
105
|
"search.open": "搜索标题",
|
|
42
106
|
"search.word": "搜索",
|
|
43
107
|
"search.tail": ",回车定位…",
|
|
44
108
|
"search.scope.title": "标题",
|
|
45
109
|
"search.scope.full": "全文",
|
|
110
|
+
"search.scope.cross": "会话",
|
|
46
111
|
"search.scope.tipTitle": "当前:仅搜索标题。点击切换为全文搜索",
|
|
47
|
-
"search.scope.tipFull": "
|
|
112
|
+
"search.scope.tipFull": "当前:全文搜索。点击切换为跨会话检索",
|
|
113
|
+
"search.scope.tipCross": "当前:跨会话检索(用宿主的全文索引搜所有会话的消息正文)。点击回到仅搜索标题",
|
|
114
|
+
"search.cross.loading": "检索中…",
|
|
115
|
+
"search.cross.empty": "其他会话里没有匹配",
|
|
116
|
+
"search.cross.error": "检索失败:",
|
|
117
|
+
"search.cross.unavailable": "宿主没有提供跨会话检索接口",
|
|
118
|
+
"search.cross.disabled": "宿主没开启会话全文索引(session-query 的 openAt 被设成 never),跨会话检索暂时用不了",
|
|
119
|
+
"search.cross.more": "宿主一次最多返回 20 条,可能还有更多",
|
|
120
|
+
"search.cross.note": "跨会话检索走宿主的全文索引,只搜消息正文,模糊开关对它不生效",
|
|
121
|
+
"search.cross.hidden": "另有 {n} 条命中属于已归档、子会话或当前不在列表里的会话,已略过(这些会话现在打不开)",
|
|
122
|
+
"search.cross.goneHit": "这条命中所在的会话现在打不开(已归档或不在会话列表里),已跳过",
|
|
123
|
+
"search.cross.switched": "已切换到该会话,同关键词已开始在本会话内查找",
|
|
124
|
+
"search.cross.noSpot": "该会话已打开,但这处位置还没加载,没能跳过去;可用搜索框继续查找",
|
|
48
125
|
"search.fuzzy": "模糊",
|
|
49
126
|
"search.fuzzy.tipOn": "模糊匹配已开启:允许关键字中间夹少量其他文字,命中更多",
|
|
50
127
|
"search.fuzzy.tipOff": "模糊匹配已关闭:只匹配连续的文字。点击开启",
|
|
@@ -54,9 +131,12 @@ window.__ModuleLoader__.load({
|
|
|
54
131
|
"hint.bottom": "已经到底了",
|
|
55
132
|
"outline.bottom": "回到底部",
|
|
56
133
|
"turn.jumpReply": "跳转到该回合的模型回答开头",
|
|
134
|
+
"row.end": "跳到本节末尾",
|
|
57
135
|
"turn.jumpTurn": "跳转到该回合开头",
|
|
58
136
|
"turn.unloaded": "未加载",
|
|
59
137
|
"turn.loadTip": "点击加载这个回合并跳转过去",
|
|
138
|
+
"turn.noMoreHistory": "宿主说这个会话没有更早的历史可翻了(它的翻页窗口已到开头),这一回合加载不出来",
|
|
139
|
+
"turn.loadFailed": "这个回合没能加载出来(这段历史可能已经不在会话日志里)",
|
|
60
140
|
"turn.failed": "请求失败",
|
|
61
141
|
"turn.jumpFailed": "跳转到该回合的报错位置",
|
|
62
142
|
"time.yesterday": "昨天",
|
|
@@ -83,6 +163,8 @@ window.__ModuleLoader__.load({
|
|
|
83
163
|
"settings.off": "关闭",
|
|
84
164
|
"settings.fuzzy": "模糊搜索",
|
|
85
165
|
"settings.hover": "悬停预览卡片",
|
|
166
|
+
"settings.remember": "记住阅读位置",
|
|
167
|
+
"settings.remember.tip": "开启后,重新打开某个会话会回到上次读到的那一轮;位置只保存在这台浏览器里",
|
|
86
168
|
"settings.debug": "在控制台打印诊断日志",
|
|
87
169
|
"settings.debug.tip": "开启后控制台会打印一行挂载诊断(宿主能力与跳转加载器状态)",
|
|
88
170
|
"settings.overridden": "已自定义",
|
|
@@ -97,16 +179,41 @@ window.__ModuleLoader__.load({
|
|
|
97
179
|
"handle.dockRight": "Move to the right",
|
|
98
180
|
"handle.collapse": "Collapse",
|
|
99
181
|
"handle.expand": "Expand the outline",
|
|
182
|
+
"a11y.list": "Outline list: arrow keys move, Enter jumps, Esc collapses the panel",
|
|
183
|
+
"a11y.hits": "Match {n} of {total}",
|
|
184
|
+
"a11y.results": "{n} result(s)",
|
|
185
|
+
"a11y.loading": "Searching",
|
|
186
|
+
"a11y.crossEmpty": "No results",
|
|
187
|
+
"sheet.open": "Full-width outline (a sheet that drops from the top)",
|
|
188
|
+
"sheet.openHandle": "Open the curtain: a full-width outline drops from the top",
|
|
189
|
+
"sheet.close": "Close the sheet",
|
|
190
|
+
"sheet.noDrag": "The sheet spans the conversation area, so it cannot be dragged or resized",
|
|
100
191
|
"levels.tip": "Heading level filter",
|
|
101
192
|
"levels.show": "Show H",
|
|
102
193
|
"levels.hide": "Hide H",
|
|
194
|
+
"levels.offInQuestions": "Questions-only view has no heading rows, so the level filter does not apply",
|
|
195
|
+
"view.questions.tipOn": "Questions only: each turn shows just its time and your prompt. Click for the full outline",
|
|
196
|
+
"view.questions.tipOff": "Full outline. Click to show each turn's time and your prompt only",
|
|
103
197
|
"search.open": "Search headings",
|
|
104
198
|
"search.word": "Search",
|
|
105
199
|
"search.tail": ", Enter to jump…",
|
|
106
200
|
"search.scope.title": "Title",
|
|
107
201
|
"search.scope.full": "Full text",
|
|
202
|
+
"search.scope.cross": "Session",
|
|
108
203
|
"search.scope.tipTitle": "Titles only. Click for full-text search",
|
|
109
|
-
"search.scope.tipFull": "Full text. Click for
|
|
204
|
+
"search.scope.tipFull": "Full text. Click for cross-session search",
|
|
205
|
+
"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",
|
|
206
|
+
"search.cross.loading": "Searching…",
|
|
207
|
+
"search.cross.empty": "No matches in other sessions",
|
|
208
|
+
"search.cross.error": "Search failed: ",
|
|
209
|
+
"search.cross.unavailable": "The host does not expose cross-session search",
|
|
210
|
+
"search.cross.disabled": "This deployment keeps the host's full-text session index off (session-query openAt: never), so cross-session search is unavailable",
|
|
211
|
+
"search.cross.more": "The host returns at most 20 results; more may exist",
|
|
212
|
+
"search.cross.note": "Cross-session search reads the host's full-text index over message text; the fuzzy switch does not apply",
|
|
213
|
+
"search.cross.hidden": "Also skipped {n} hit(s) from archived, subagent or unlisted sessions — they cannot be opened",
|
|
214
|
+
"search.cross.goneHit": "That hit's session cannot be opened right now (archived or not in the session list); skipped",
|
|
215
|
+
"search.cross.switched": "Switched to that session and started the same keyword search here",
|
|
216
|
+
"search.cross.noSpot": "The session is open, but that spot is not loaded yet — the jump was skipped; keep searching in the box",
|
|
110
217
|
"search.fuzzy": "Fuzzy",
|
|
111
218
|
"search.fuzzy.tipOn": "Fuzzy matching is on: a few other characters may sit between the keywords, so more matches hit",
|
|
112
219
|
"search.fuzzy.tipOff": "Fuzzy matching is off: only contiguous text matches. Click to turn it on",
|
|
@@ -116,9 +223,12 @@ window.__ModuleLoader__.load({
|
|
|
116
223
|
"hint.bottom": "You have reached the end",
|
|
117
224
|
"outline.bottom": "Back to the newest entry",
|
|
118
225
|
"turn.jumpReply": "Jump to the start of this turn's model reply",
|
|
226
|
+
"row.end": "Jump to the end of this section",
|
|
119
227
|
"turn.jumpTurn": "Jump to the start of this turn",
|
|
120
228
|
"turn.unloaded": "Not loaded",
|
|
121
229
|
"turn.loadTip": "Click to load this turn and jump to it",
|
|
230
|
+
"turn.noMoreHistory": "The host reports no earlier history left for this session (its page window is already at the start), so this turn cannot be paged in",
|
|
231
|
+
"turn.loadFailed": "That turn could not be loaded (its history may no longer be in the session log)",
|
|
122
232
|
"turn.failed": "Request failed",
|
|
123
233
|
"turn.jumpFailed": "Jump to this turn's error",
|
|
124
234
|
// NB: "time.beforeYesterday" stays Chinese-only on purpose — English has no
|
|
@@ -147,6 +257,8 @@ window.__ModuleLoader__.load({
|
|
|
147
257
|
"settings.off": "Off",
|
|
148
258
|
"settings.fuzzy": "Fuzzy search",
|
|
149
259
|
"settings.hover": "Hover preview card",
|
|
260
|
+
"settings.remember": "Remember the reading position",
|
|
261
|
+
"settings.remember.tip": "When on, reopening a session returns to the turn you were last reading; the position is stored in this browser only",
|
|
150
262
|
"settings.debug": "Print the mount diagnostic to the console",
|
|
151
263
|
"settings.debug.tip": "The console then prints one mount line (host capabilities and jump-loader state)",
|
|
152
264
|
"settings.overridden": "Customized",
|
|
@@ -177,6 +289,16 @@ window.__ModuleLoader__.load({
|
|
|
177
289
|
if (s === undefined && activeLang !== "zh") s = DICTS.zh[key];
|
|
178
290
|
return s === undefined ? key : s;
|
|
179
291
|
}
|
|
292
|
+
// Same lookup, with `{name}` placeholders filled in. Kept separate from T so the
|
|
293
|
+
// host-locale path (which returns a plain string) still works: a translation that
|
|
294
|
+
// drops a placeholder simply loses that number rather than printing the braces.
|
|
295
|
+
function Tp(key, params) {
|
|
296
|
+
var s = T(key);
|
|
297
|
+
if (typeof s !== "string" || params === undefined || params === null) return s;
|
|
298
|
+
return s.replace(/\{(\w+)\}/g, function (match, name) {
|
|
299
|
+
return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
180
302
|
// The host reports the language it resolved for us; `auto` follows it, an explicit
|
|
181
303
|
// preference wins, and an unknown host language falls back to Chinese.
|
|
182
304
|
function setLanguage(pref, hostLang) {
|
|
@@ -209,14 +331,15 @@ window.__ModuleLoader__.load({
|
|
|
209
331
|
// purpose — they describe THIS screen, not the account.
|
|
210
332
|
var NAMESPACE = "dsh-quick-toc";
|
|
211
333
|
// Fields backed by the Host settings document; everything else is localStorage.
|
|
212
|
-
var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, zoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1 };
|
|
334
|
+
var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, zoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1 };
|
|
213
335
|
// The schema defaults (mirrored here for the card's per-field reset and for the
|
|
214
336
|
// one-time import of a 0.5.x install's stored values).
|
|
215
337
|
var DEFAULTS = {
|
|
216
338
|
dock: "left", y: 0, w: 0, h: 0,
|
|
217
339
|
levels: [1, 2, 3, 4, 5, 6],
|
|
218
340
|
zoom: 1, handle: 0.5,
|
|
219
|
-
fuzzy: false, hover: true, lang: "auto", debug: false
|
|
341
|
+
fuzzy: false, hover: true, lang: "auto", debug: false,
|
|
342
|
+
remember: true
|
|
220
343
|
};
|
|
221
344
|
var PREF_KEYS = {
|
|
222
345
|
dock: "dsh-quick-toc.dock.v2",
|
|
@@ -229,6 +352,7 @@ window.__ModuleLoader__.load({
|
|
|
229
352
|
fuzzy: "dsh-quick-toc.fuzzy.v1",
|
|
230
353
|
hover: "dsh-quick-toc.hover.v1",
|
|
231
354
|
lang: "dsh-quick-toc.lang.v1",
|
|
355
|
+
remember: "dsh-quick-toc.remember.v1",
|
|
232
356
|
debug: "dsh-quick-toc.debug"
|
|
233
357
|
};
|
|
234
358
|
// Writing a field's schema default through the card IS its reset — a value that
|
|
@@ -247,6 +371,54 @@ window.__ModuleLoader__.load({
|
|
|
247
371
|
};
|
|
248
372
|
var OLD_MAX_LEVEL_KEY = "dsh-quick-toc.maxLevel.v1";
|
|
249
373
|
var LEVELS_ALL = [1, 2, 3, 4, 5, 6];
|
|
374
|
+
// ---- per-session reading position (browser-local, like panelW/H) ----------
|
|
375
|
+
// One small map in localStorage: sessionId -> { turn, seq, at }. Turn numbers are
|
|
376
|
+
// the stable coordinate (a node key can disappear with a re-framed log; the turn is
|
|
377
|
+
// what the host's turn index and the jump loader both speak), and `seq` is the
|
|
378
|
+
// turn's own start seq — the jump loader needs it to page a turn back in, so a
|
|
379
|
+
// remembered position outside the loaded window can actually be restored.
|
|
380
|
+
var READ_POS_KEY = "dsh-quick-toc.readPos.v1";
|
|
381
|
+
var READ_POS_MAX = 200;
|
|
382
|
+
// How long a remembered position stays worth resuming. "Where was I" is a
|
|
383
|
+
// convenience within a working session; coming back hours later to a stale anchor
|
|
384
|
+
// is how the reader ends up somewhere they did not ask for (they expect the newest
|
|
385
|
+
// messages). Past this window the position is dropped and the session opens at the
|
|
386
|
+
// bottom, where the live content is.
|
|
387
|
+
var READ_POS_TTL_MS = 30 * 60 * 1000;
|
|
388
|
+
var readPosMap = function () {
|
|
389
|
+
try {
|
|
390
|
+
var raw = localStorage.getItem(READ_POS_KEY);
|
|
391
|
+
var parsed = raw ? JSON.parse(raw) : null;
|
|
392
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
393
|
+
} catch (e) { return {}; }
|
|
394
|
+
};
|
|
395
|
+
var readPos = function (sessionId) {
|
|
396
|
+
if (!sessionId) return null;
|
|
397
|
+
var entry = readPosMap()[sessionId];
|
|
398
|
+
return entry && typeof entry.turn === "number" ? entry : null;
|
|
399
|
+
};
|
|
400
|
+
var writeReadPos = function (sessionId, turn, seq) {
|
|
401
|
+
if (!sessionId || typeof turn !== "number") return;
|
|
402
|
+
try {
|
|
403
|
+
var map = readPosMap();
|
|
404
|
+
map[sessionId] = typeof seq === "number" ? { turn: turn, seq: seq, at: Date.now() } : { turn: turn, at: Date.now() };
|
|
405
|
+
var ids = Object.keys(map);
|
|
406
|
+
if (ids.length > READ_POS_MAX) {
|
|
407
|
+
ids.sort(function (a, b) { return (map[a].at || 0) - (map[b].at || 0); });
|
|
408
|
+
for (var i = 0; i < ids.length - READ_POS_MAX; i++) delete map[ids[i]];
|
|
409
|
+
}
|
|
410
|
+
localStorage.setItem(READ_POS_KEY, JSON.stringify(map));
|
|
411
|
+
} catch (e) { /* storage full or blocked: a remembered position is a convenience */ }
|
|
412
|
+
};
|
|
413
|
+
var clearReadPos = function (sessionId) {
|
|
414
|
+
if (!sessionId) return;
|
|
415
|
+
try {
|
|
416
|
+
var map = readPosMap();
|
|
417
|
+
if (!Object.prototype.hasOwnProperty.call(map, sessionId)) return;
|
|
418
|
+
delete map[sessionId];
|
|
419
|
+
localStorage.setItem(READ_POS_KEY, JSON.stringify(map));
|
|
420
|
+
} catch (e) {}
|
|
421
|
+
};
|
|
250
422
|
var prefsStore = (function () {
|
|
251
423
|
var hasOwn = function (obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); };
|
|
252
424
|
var read = function (key, legacy) {
|
|
@@ -310,6 +482,8 @@ window.__ModuleLoader__.load({
|
|
|
310
482
|
fuzzy: read(PREF_KEYS.fuzzy) === "1",
|
|
311
483
|
// absent means "on": the hover card predates this switch
|
|
312
484
|
hover: read(PREF_KEYS.hover) !== "0",
|
|
485
|
+
// absent means "on" too (the reader asked for this to default on)
|
|
486
|
+
remember: read(PREF_KEYS.remember) !== "0",
|
|
313
487
|
lang: langRaw === "zh" || langRaw === "en" ? langRaw : "auto",
|
|
314
488
|
debug: read(PREF_KEYS.debug) === "1"
|
|
315
489
|
};
|
|
@@ -325,6 +499,7 @@ window.__ModuleLoader__.load({
|
|
|
325
499
|
handle: function (v) { return String(v); },
|
|
326
500
|
fuzzy: function (v) { return v ? "1" : "0"; },
|
|
327
501
|
hover: function (v) { return v ? "1" : "0"; },
|
|
502
|
+
remember: function (v) { return v ? "1" : "0"; },
|
|
328
503
|
lang: function (v) { return v; },
|
|
329
504
|
debug: function (v) { return v ? "1" : "0"; }
|
|
330
505
|
};
|
|
@@ -355,6 +530,7 @@ window.__ModuleLoader__.load({
|
|
|
355
530
|
handle: isFinite(handleNum) ? Math.min(1, Math.max(0, Math.round(handleNum * 100) / 100)) : 0.5,
|
|
356
531
|
fuzzy: v.fuzzy === true,
|
|
357
532
|
hover: v.hover !== false,
|
|
533
|
+
remember: v.remember !== false,
|
|
358
534
|
debug: v.debug === true
|
|
359
535
|
};
|
|
360
536
|
};
|
|
@@ -365,7 +541,7 @@ window.__ModuleLoader__.load({
|
|
|
365
541
|
return {
|
|
366
542
|
dock: over.dock, y: local.y, w: local.w, h: local.h,
|
|
367
543
|
levels: over.levels, zoom: over.zoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
|
|
368
|
-
lang: over.lang, debug: over.debug
|
|
544
|
+
lang: over.lang, remember: over.remember, debug: over.debug
|
|
369
545
|
};
|
|
370
546
|
};
|
|
371
547
|
var notify = function () {
|
|
@@ -1185,7 +1361,7 @@ window.__ModuleLoader__.load({
|
|
|
1185
1361
|
position: "fixed",
|
|
1186
1362
|
top: 80,
|
|
1187
1363
|
right: 16,
|
|
1188
|
-
zIndex:
|
|
1364
|
+
zIndex: Z_ERROR,
|
|
1189
1365
|
background: "rgba(120,30,30,0.95)",
|
|
1190
1366
|
border: "1px solid rgba(255,120,120,0.4)",
|
|
1191
1367
|
color: "#ffdada",
|
|
@@ -1210,6 +1386,13 @@ window.__ModuleLoader__.load({
|
|
|
1210
1386
|
// different number of hooks makes React abort the whole panel with
|
|
1211
1387
|
// "Rendered more hooks than during the previous render" (#310). The hooks all
|
|
1212
1388
|
// live in the body below, which is only ever mounted with complete props.
|
|
1389
|
+
// Cross-session hand-off. A host search hit is `{sessionId, snippet}` with no
|
|
1390
|
+
// seq, so the panel cannot scroll to it: it switches the session and leaves this
|
|
1391
|
+
// request here, and the panel that comes up for that session re-runs the keyword
|
|
1392
|
+
// and jumps to the first match it can actually reach. Module scope on purpose —
|
|
1393
|
+
// the panel for the new session may be a fresh mount.
|
|
1394
|
+
var pendingLocate = null;
|
|
1395
|
+
|
|
1213
1396
|
function OutlinePanel(props) {
|
|
1214
1397
|
if (!props.useChat) {
|
|
1215
1398
|
console.warn("[dsh-quick-toc] useChat prop missing (requires DSH >= 0.1.5-rc.1)");
|
|
@@ -1251,6 +1434,10 @@ window.__ModuleLoader__.load({
|
|
|
1251
1434
|
};
|
|
1252
1435
|
// Which stage of the bridge is actually ready — reported once at mount so a
|
|
1253
1436
|
// missing jump loader can be diagnosed from the console instead of guessed.
|
|
1437
|
+
// Past "the loader exists" the PAGE STATE is what decides whether a jump can do
|
|
1438
|
+
// anything: a window the host considers complete (hasMore false) or one whose
|
|
1439
|
+
// base already covers the target makes loadThrough resolve without loading, and
|
|
1440
|
+
// from the outline that is indistinguishable from "loading stopped working".
|
|
1254
1441
|
var jumpLoaderState = function () {
|
|
1255
1442
|
var sessions = hostSessions();
|
|
1256
1443
|
if (!sessions) return "no-sessions-service";
|
|
@@ -1263,7 +1450,9 @@ window.__ModuleLoader__.load({
|
|
|
1263
1450
|
}
|
|
1264
1451
|
var face = binding && binding.session;
|
|
1265
1452
|
if (!face) return "no-binding-for-session";
|
|
1266
|
-
|
|
1453
|
+
if (typeof face.loadThrough !== "function") return "no-loadThrough";
|
|
1454
|
+
return "ready(open=" + String(face.openState) + ",hasMore=" + String(face.hasMore)
|
|
1455
|
+
+ ",baseSeq=" + String(face.baseSeq) + ")";
|
|
1267
1456
|
};
|
|
1268
1457
|
|
|
1269
1458
|
// Structural narrowing of that projection (its value crosses the wire):
|
|
@@ -1300,23 +1489,47 @@ window.__ModuleLoader__.load({
|
|
|
1300
1489
|
var viewport = _s2[0];
|
|
1301
1490
|
var setViewport = _s2[1];
|
|
1302
1491
|
react.useEffect(function () {
|
|
1492
|
+
// Self-healing, because the layout AROUND the conversation can change without
|
|
1493
|
+
// a resize: the host's own controls move that column about (measured in a real
|
|
1494
|
+
// browser: 40px of margin-left on the centre column shifted the scroller's
|
|
1495
|
+
// rect, no window resize was involved, and the plugin followed on this beat).
|
|
1496
|
+
// A stale rect parks the panel — and its collapse handle — on a line that no
|
|
1497
|
+
// longer exists, which reads as "the panel cannot be closed any more". The
|
|
1498
|
+
// observer is re-attached whenever the host swaps the scroller node out.
|
|
1499
|
+
var observed = null;
|
|
1500
|
+
var obs = null;
|
|
1501
|
+
var publish = function (next) {
|
|
1502
|
+
// the observer fires often; publishing an identical box would re-render
|
|
1503
|
+
// the whole list for nothing
|
|
1504
|
+
setViewport(function (prev) {
|
|
1505
|
+
if (prev &&
|
|
1506
|
+
Math.abs(prev.left - next.left) < 1 && Math.abs(prev.right - next.right) < 1 &&
|
|
1507
|
+
Math.abs(prev.top - next.top) < 1 && Math.abs(prev.height - next.height) < 1) {
|
|
1508
|
+
return prev;
|
|
1509
|
+
}
|
|
1510
|
+
return next;
|
|
1511
|
+
});
|
|
1512
|
+
};
|
|
1303
1513
|
var compute = function () {
|
|
1304
1514
|
var sp = document.querySelector("[data-conversation-scroll]");
|
|
1305
|
-
if (sp)
|
|
1306
|
-
|
|
1307
|
-
|
|
1515
|
+
if (!sp) return;
|
|
1516
|
+
if (sp !== observed) { // a replaced scroller: the old observation is worthless
|
|
1517
|
+
observed = sp;
|
|
1518
|
+
if (obs) obs.disconnect();
|
|
1519
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1520
|
+
obs = new ResizeObserver(function () { compute(); });
|
|
1521
|
+
obs.observe(sp);
|
|
1522
|
+
}
|
|
1308
1523
|
}
|
|
1524
|
+
var r = sp.getBoundingClientRect();
|
|
1525
|
+
publish({ left: r.left, right: window.innerWidth - r.right, top: r.top, height: r.height });
|
|
1309
1526
|
};
|
|
1310
1527
|
compute();
|
|
1311
1528
|
window.addEventListener("resize", compute);
|
|
1312
|
-
var
|
|
1313
|
-
var obs = null;
|
|
1314
|
-
if (typeof ResizeObserver !== "undefined" && sp) {
|
|
1315
|
-
obs = new ResizeObserver(compute);
|
|
1316
|
-
obs.observe(sp);
|
|
1317
|
-
}
|
|
1529
|
+
var beat = setInterval(compute, 500);
|
|
1318
1530
|
return function () {
|
|
1319
1531
|
window.removeEventListener("resize", compute);
|
|
1532
|
+
clearInterval(beat);
|
|
1320
1533
|
if (obs) obs.disconnect();
|
|
1321
1534
|
};
|
|
1322
1535
|
}, []);
|
|
@@ -1336,18 +1549,47 @@ window.__ModuleLoader__.load({
|
|
|
1336
1549
|
var setPanelY = _s4[1];
|
|
1337
1550
|
var handleRef = react.useRef(null); // panel top drag bar
|
|
1338
1551
|
var edgeRef = react.useRef(null); // edge collapse handle
|
|
1552
|
+
var sheetHandleRef = react.useRef(null); // curtain handle along the top edge
|
|
1339
1553
|
|
|
1340
1554
|
// handle reveal: after the panel fully slides away, the handle fades+pops in
|
|
1341
1555
|
var _s5 = react.useState(false);
|
|
1342
1556
|
var handleShown = _s5[0];
|
|
1343
1557
|
var setHandleShown = _s5[1];
|
|
1558
|
+
// ...and it is NOT unmounted the instant it is clicked: the collapse-side handle
|
|
1559
|
+
// used to disappear on the click's own frame, which read as a hard cut. It stays
|
|
1560
|
+
// mounted for the length of its fade-out and only then leaves the tree.
|
|
1561
|
+
var _s5b = react.useState(false);
|
|
1562
|
+
var edgeGone = _s5b[0];
|
|
1563
|
+
var setEdgeGone = _s5b[1];
|
|
1564
|
+
// ...and the panel itself waits for it: clicking the edge handle used to start the
|
|
1565
|
+
// panel's slide on the same frame, so the two crossed over and the panel looked
|
|
1566
|
+
// like it came out before the handle had gone in. While `edgeLeaving` is set the
|
|
1567
|
+
// panel's transition is delayed, so the handle slips home first and only then does
|
|
1568
|
+
// the panel slide out. (Its inline geometry is the open one from the first frame —
|
|
1569
|
+
// only the animation is held back — so nothing else has to move.)
|
|
1570
|
+
var _s5c = react.useState(false);
|
|
1571
|
+
var edgeLeaving = _s5c[0];
|
|
1572
|
+
var setEdgeLeaving = _s5c[1];
|
|
1573
|
+
var openFromEdge = function () {
|
|
1574
|
+
if (open) return;
|
|
1575
|
+
setEdgeLeaving(true);
|
|
1576
|
+
setOpen(true);
|
|
1577
|
+
};
|
|
1578
|
+
react.useEffect(function () {
|
|
1579
|
+
if (!edgeLeaving) return;
|
|
1580
|
+
var t = setTimeout(function () { setEdgeLeaving(false); }, EDGE_OUT_MS);
|
|
1581
|
+
return function () { clearTimeout(t); };
|
|
1582
|
+
}, [edgeLeaving]);
|
|
1344
1583
|
react.useEffect(function () {
|
|
1345
1584
|
if (!open) {
|
|
1346
1585
|
setHandleShown(false);
|
|
1586
|
+
setEdgeGone(false);
|
|
1347
1587
|
var t = setTimeout(function () { setHandleShown(true); }, 300); // right after the panel slides away
|
|
1348
1588
|
return function () { clearTimeout(t); };
|
|
1349
1589
|
}
|
|
1350
1590
|
setHandleShown(false);
|
|
1591
|
+
var t2 = setTimeout(function () { setEdgeGone(true); }, EDGE_OUT_MS + 30); // the fade-out, then unmount
|
|
1592
|
+
return function () { clearTimeout(t2); };
|
|
1351
1593
|
}, [open]);
|
|
1352
1594
|
// panel hover: clear when mouse inside, transparent when outside
|
|
1353
1595
|
var _s6 = react.useState(false);
|
|
@@ -1578,6 +1820,202 @@ window.__ModuleLoader__.load({
|
|
|
1578
1820
|
};
|
|
1579
1821
|
}, []);
|
|
1580
1822
|
|
|
1823
|
+
// ---- questions-only view ------------------------------------------------
|
|
1824
|
+
// A second way to read a long session: keep every turn's header (time + the
|
|
1825
|
+
// prompt) and drop the heading rows. The switch is a cross-fade, not a swap:
|
|
1826
|
+
// the list fades out, the mode changes while it is invisible, then it fades
|
|
1827
|
+
// back in — the two lists never blend into each other mid-swap. Session-local
|
|
1828
|
+
// on purpose (a reading mode, like the open search box, not a stored
|
|
1829
|
+
// preference).
|
|
1830
|
+
var _sQ = react.useState(false);
|
|
1831
|
+
var questionsOnly = _sQ[0];
|
|
1832
|
+
var setQuestionsOnly = _sQ[1];
|
|
1833
|
+
var _sQF = react.useState("idle"); // "idle" | "out" | "in"
|
|
1834
|
+
var listFade = _sQF[0];
|
|
1835
|
+
var setListFade = _sQF[1];
|
|
1836
|
+
var listFadeTimers = react.useRef({ out: null, in: null });
|
|
1837
|
+
var toggleQuestions = function () {
|
|
1838
|
+
if (listFadeTimers.current.out) clearTimeout(listFadeTimers.current.out);
|
|
1839
|
+
if (listFadeTimers.current.in) clearTimeout(listFadeTimers.current.in);
|
|
1840
|
+
// reduce motion is a real preference: swap without the two-phase fade
|
|
1841
|
+
var calm = false;
|
|
1842
|
+
try {
|
|
1843
|
+
calm = typeof window !== "undefined" && typeof window.matchMedia === "function"
|
|
1844
|
+
&& window.matchMedia("(prefers-reduced-motion: reduce)").matches === true;
|
|
1845
|
+
} catch (e) { calm = false; }
|
|
1846
|
+
if (calm) {
|
|
1847
|
+
setQuestionsOnly(function (cur) { return !cur; });
|
|
1848
|
+
setListFade("idle");
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
setListFade("out");
|
|
1852
|
+
listFadeTimers.current.out = setTimeout(function () {
|
|
1853
|
+
listFadeTimers.current.out = null;
|
|
1854
|
+
setQuestionsOnly(function (cur) { return !cur; });
|
|
1855
|
+
// entering questions-only makes the level filter meaningless: close its
|
|
1856
|
+
// popup rather than leave a disabled control's menu open over the list
|
|
1857
|
+
if (!questionsOnly && typeof closeLevels === "function") closeLevels();
|
|
1858
|
+
setListFade("in");
|
|
1859
|
+
listFadeTimers.current.in = setTimeout(function () {
|
|
1860
|
+
listFadeTimers.current.in = null;
|
|
1861
|
+
setListFade("idle");
|
|
1862
|
+
}, 260);
|
|
1863
|
+
}, 150);
|
|
1864
|
+
};
|
|
1865
|
+
react.useEffect(function () {
|
|
1866
|
+
return function () {
|
|
1867
|
+
if (listFadeTimers.current.out) clearTimeout(listFadeTimers.current.out);
|
|
1868
|
+
if (listFadeTimers.current.in) clearTimeout(listFadeTimers.current.in);
|
|
1869
|
+
};
|
|
1870
|
+
}, []);
|
|
1871
|
+
|
|
1872
|
+
// ---- keyboard navigation -------------------------------------------------
|
|
1873
|
+
// The list is a real widget: ↑/↓ walk its rows in visual order, Home/End jump
|
|
1874
|
+
// to the ends, Enter activates the row under the cursor (each row keeps its own
|
|
1875
|
+
// click semantics — jump / page-in / switch session), Esc collapses the panel.
|
|
1876
|
+
// The cursor is a keyboard artefact, NOT the reading position: the blue box
|
|
1877
|
+
// follows the conversation, this outline follows the keys, and they may sit on
|
|
1878
|
+
// different rows. -1 = no cursor (nothing is painted).
|
|
1879
|
+
var _sNav = react.useState(-1);
|
|
1880
|
+
var navIdx = _sNav[0];
|
|
1881
|
+
var setNavIdx = _sNav[1];
|
|
1882
|
+
// Polite live region: match stepping, result counts and cursor moves are
|
|
1883
|
+
// announced for screen readers (and read out by any a11y layer the host adds).
|
|
1884
|
+
var _sLive = react.useState("");
|
|
1885
|
+
var liveText = _sLive[0];
|
|
1886
|
+
var setLiveText = _sLive[1];
|
|
1887
|
+
|
|
1888
|
+
// ---- full-width sheet ("幕布") -------------------------------------------
|
|
1889
|
+
// The same panel content, dropped from the top of the conversation area at full
|
|
1890
|
+
// width. It REPLACES the docked panel while open (the list keeps its ref, so
|
|
1891
|
+
// scrolling, page-in, the keyboard cursor and the hover card all keep working
|
|
1892
|
+
// against one element, not two).
|
|
1893
|
+
var _sSheet = react.useState(false);
|
|
1894
|
+
var sheetOpen = _sSheet[0];
|
|
1895
|
+
var setSheetOpen = _sSheet[1];
|
|
1896
|
+
var sheetRef = react.useRef(null);
|
|
1897
|
+
var sheetTimerRef = react.useRef(null);
|
|
1898
|
+
// Handing the docked panel over to the curtain. They are ONE list — while the
|
|
1899
|
+
// curtain is down the panel element IS the curtain's content — so switching
|
|
1900
|
+
// surfaces used to make the panel teleport: gone from the dock on the way
|
|
1901
|
+
// down, popping straight back in when the curtain retracted. The reader read
|
|
1902
|
+
// that as a flash. Now the panel always travels the way it always does:
|
|
1903
|
+
// while it is "aside" it renders at its collapsed spot and the existing
|
|
1904
|
+
// left/clip transition does the moving — it slides home just before the
|
|
1905
|
+
// curtain drops ("tuck"), and slides back in just after the curtain is gone
|
|
1906
|
+
// ("return") instead of popping into place.
|
|
1907
|
+
// "tuck" panel sliding home BEFORE the curtain opens (state must persist)
|
|
1908
|
+
// "return" panel sliding back in AFTER the curtain closed (one painted frame)
|
|
1909
|
+
var _sAside = react.useState(null);
|
|
1910
|
+
var panelAside = _sAside[0];
|
|
1911
|
+
var setPanelAside = _sAside[1];
|
|
1912
|
+
// The curtain hangs from just BELOW the centre column's view tabs (对话 / 轨迹 /
|
|
1913
|
+
// 上下文), not from the top of the scroll container: when those tabs live inside
|
|
1914
|
+
// that container, viewport.top sits above them and the curtain would cover them.
|
|
1915
|
+
// Both edges of that strip are kept: the curtain hangs from its BOTTOM line, and
|
|
1916
|
+
// the curtain handle sits INSIDE the strip with its own bottom on that same line
|
|
1917
|
+
// (that band is host chrome, so no conversation text can ever run under it —
|
|
1918
|
+
// which is exactly what made the handle unreadable when it hung below the line).
|
|
1919
|
+
// Measured when the conversation is (re)measured, not on every render —
|
|
1920
|
+
// getBoundingClientRect is a layout read.
|
|
1921
|
+
var _sSheetTop = react.useState(null);
|
|
1922
|
+
var sheetStrip = _sSheetTop[0]; // { top, bottom } | null
|
|
1923
|
+
var setSheetTop = _sSheetTop[1];
|
|
1924
|
+
// The curtain handle is held in the tree for DROP_OUT_MS after the curtain starts
|
|
1925
|
+
// opening, so its press-flat is actually seen (the curtain opens and would
|
|
1926
|
+
// otherwise unmount it in the same commit).
|
|
1927
|
+
var _sDropGone = react.useState(false);
|
|
1928
|
+
var sheetHandleGone = _sDropGone[0];
|
|
1929
|
+
var setSheetHandleGone = _sDropGone[1];
|
|
1930
|
+
// the droplet's click may defer the curtain until its press has played (see the
|
|
1931
|
+
// handle's onClick): one timer, always cancelled before being re-armed
|
|
1932
|
+
var dropOpenRef = react.useRef(null);
|
|
1933
|
+
react.useEffect(function () { return function () { if (dropOpenRef.current) clearTimeout(dropOpenRef.current); }; }, []);
|
|
1934
|
+
react.useEffect(function () {
|
|
1935
|
+
if (!sheetOpen) { setSheetHandleGone(false); return; }
|
|
1936
|
+
var t = setTimeout(function () { setSheetHandleGone(true); }, DROP_OUT_MS);
|
|
1937
|
+
return function () { clearTimeout(t); };
|
|
1938
|
+
}, [sheetOpen]);
|
|
1939
|
+
react.useEffect(function () {
|
|
1940
|
+
if (!viewport) return;
|
|
1941
|
+
var tabs = typeof conversationTablist === "function" ? conversationTablist() : null;
|
|
1942
|
+
var next = null;
|
|
1943
|
+
if (tabs && typeof tabs.getBoundingClientRect === "function") {
|
|
1944
|
+
var r = tabs.getBoundingClientRect();
|
|
1945
|
+
if (r && r.height > 0 && r.bottom > 0) next = { top: r.top, bottom: r.bottom };
|
|
1946
|
+
}
|
|
1947
|
+
setSheetTop(next);
|
|
1948
|
+
}, [sheetOpen, viewport]);
|
|
1949
|
+
react.useEffect(function () {
|
|
1950
|
+
return function () {
|
|
1951
|
+
if (sheetTimerRef.current) clearTimeout(sheetTimerRef.current);
|
|
1952
|
+
};
|
|
1953
|
+
}, []);
|
|
1954
|
+
// The retract is driven by a CLASS on the DOM node rather than by state: a
|
|
1955
|
+
// state change re-renders the whole (possibly huge) list on the very frame the
|
|
1956
|
+
// animation starts, and that long task is what made the retract stutter. React
|
|
1957
|
+
// only rewrites `class` when the prop changes, so a class added here survives
|
|
1958
|
+
// re-renders until the curtain is actually gone.
|
|
1959
|
+
var closeSheet = function () {
|
|
1960
|
+
if (!sheetOpen || sheetTimerRef.current) return;
|
|
1961
|
+
var el = sheetRef.current;
|
|
1962
|
+
if (el && el.classList) {
|
|
1963
|
+
el.classList.remove("dqt-sheet-open");
|
|
1964
|
+
el.classList.add("dqt-sheet-closing");
|
|
1965
|
+
}
|
|
1966
|
+
// the timer sits just past the exit animation (0.3s): there is no
|
|
1967
|
+
// animationend listener, so this is what actually closes the curtain
|
|
1968
|
+
sheetTimerRef.current = setTimeout(function () {
|
|
1969
|
+
sheetTimerRef.current = null;
|
|
1970
|
+
setSheetOpen(false);
|
|
1971
|
+
// the panel was open before the curtain: slide it back in instead of
|
|
1972
|
+
// letting it pop into place (the reader saw that pop as a flash)
|
|
1973
|
+
if (open) setPanelAside("return");
|
|
1974
|
+
}, 340);
|
|
1975
|
+
};
|
|
1976
|
+
var openSheet = function () {
|
|
1977
|
+
if (sheetOpen || panelAside) return;
|
|
1978
|
+
if (sheetTimerRef.current) { clearTimeout(sheetTimerRef.current); sheetTimerRef.current = null; }
|
|
1979
|
+
// reopening mid-retract: the node still carries the exit class and React
|
|
1980
|
+
// would not put the enter class back on its own (that prop never changed)
|
|
1981
|
+
var restoreEnterClass = function () {
|
|
1982
|
+
var node = sheetRef.current;
|
|
1983
|
+
if (node && node.classList) {
|
|
1984
|
+
node.classList.remove("dqt-sheet-closing");
|
|
1985
|
+
node.classList.add("dqt-sheet-open");
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
if (open) {
|
|
1989
|
+
// The docked panel goes home FIRST — its own collapse motion, start to
|
|
1990
|
+
// finish — and only then does the curtain drop. Two reasons, both seen in
|
|
1991
|
+
// the browser: (1) the enter animation used to be armed at CLICK time, so
|
|
1992
|
+
// by the time the curtain became visible most of its 0.46s drop was
|
|
1993
|
+
// already spent and it looked like it snapped into place; (2) the panel
|
|
1994
|
+
// was pulled into the sheet mid-slide, which read as it flashing away.
|
|
1995
|
+
setPanelAside("tuck");
|
|
1996
|
+
sheetTimerRef.current = setTimeout(function () {
|
|
1997
|
+
sheetTimerRef.current = null;
|
|
1998
|
+
restoreEnterClass(); // the drop animation starts HERE, with the first paint
|
|
1999
|
+
setSheetOpen(true);
|
|
2000
|
+
setPanelAside(null);
|
|
2001
|
+
}, PANEL_TUCK_MS);
|
|
2002
|
+
} else {
|
|
2003
|
+
restoreEnterClass();
|
|
2004
|
+
setSheetOpen(true);
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
// "return" only needs to exist for one painted frame: the panel renders at
|
|
2008
|
+
// its collapsed spot, and releasing it lets the left/clip transition carry
|
|
2009
|
+
// it back open. The tuck must NOT be released here — it is held until the
|
|
2010
|
+
// curtain actually opens.
|
|
2011
|
+
react.useEffect(function () {
|
|
2012
|
+
if (panelAside !== "return") return;
|
|
2013
|
+
var raf = requestAnimationFrame(function () {
|
|
2014
|
+
raf = requestAnimationFrame(function () { setPanelAside(null); });
|
|
2015
|
+
});
|
|
2016
|
+
return function () { cancelAnimationFrame(raf); };
|
|
2017
|
+
}, [panelAside]);
|
|
2018
|
+
|
|
1581
2019
|
// ---- search: header button opens a keyword box; Enter cycles through
|
|
1582
2020
|
// matching headings and jumps to each ----
|
|
1583
2021
|
var _s13 = react.useState(false);
|
|
@@ -1608,9 +2046,18 @@ window.__ModuleLoader__.load({
|
|
|
1608
2046
|
var prevScope = _s19[0];
|
|
1609
2047
|
var setPrevScope = _s19[1];
|
|
1610
2048
|
var scopeTimerRef = react.useRef(null);
|
|
2049
|
+
// search scope: "title" (default) / "full" (user messages + AI replies in this
|
|
2050
|
+
// session) / "cross" (the HOST's full-text index across every visible session).
|
|
2051
|
+
// ONE pill cycles the three, so a third scope costs no extra button.
|
|
2052
|
+
var scopeLabel = function (scope) {
|
|
2053
|
+
if (scope === "full") return T("search.scope.full");
|
|
2054
|
+
if (scope === "cross") return T("search.scope.cross");
|
|
2055
|
+
return T("search.scope.title");
|
|
2056
|
+
};
|
|
1611
2057
|
var toggleScope = function () {
|
|
2058
|
+
var next = searchScope === "title" ? "full" : (searchScope === "full" ? "cross" : "title");
|
|
1612
2059
|
setPrevScope(searchScope);
|
|
1613
|
-
setSearchScope(
|
|
2060
|
+
setSearchScope(next);
|
|
1614
2061
|
setMatchIdx(0);
|
|
1615
2062
|
if (scopeTimerRef.current) clearTimeout(scopeTimerRef.current);
|
|
1616
2063
|
scopeTimerRef.current = setTimeout(function () { setPrevScope(null); }, 300);
|
|
@@ -1626,6 +2073,59 @@ window.__ModuleLoader__.load({
|
|
|
1626
2073
|
setMatchIdx(0);
|
|
1627
2074
|
};
|
|
1628
2075
|
|
|
2076
|
+
// ---- cross-session search (the host's own full-text index) --------------
|
|
2077
|
+
// "cross" scope asks the HOST to search message text across every visible
|
|
2078
|
+
// session. Two host facts shape the code below: one search returns at most
|
|
2079
|
+
// `searchResultLimit` (20) items plus a `hasMore` flag, and an item carries
|
|
2080
|
+
// only `{sessionId, snippet}` — NO seq. So a hit cannot be jumped to directly:
|
|
2081
|
+
// clicking one switches to that session and re-runs the same keyword here (the
|
|
2082
|
+
// pendingLocate hand-off below).
|
|
2083
|
+
var _sCross = react.useState({ phase: "idle", rows: [], more: false, error: "" });
|
|
2084
|
+
var cross = _sCross[0];
|
|
2085
|
+
var setCross = _sCross[1];
|
|
2086
|
+
var crossAbortRef = react.useRef(null);
|
|
2087
|
+
var crossTimerRef = react.useRef(null);
|
|
2088
|
+
var crossTitle = function (id) {
|
|
2089
|
+
var sessions = hostSessions();
|
|
2090
|
+
try {
|
|
2091
|
+
var snap = sessions && sessions.list && typeof sessions.list.getSnapshot === "function"
|
|
2092
|
+
? sessions.list.getSnapshot() : null;
|
|
2093
|
+
var rec = snap && snap.byId ? snap.byId[id] : null;
|
|
2094
|
+
if (rec && typeof rec.displayTitle === "string" && rec.displayTitle !== "") return rec.displayTitle;
|
|
2095
|
+
} catch (e) { /* title is decoration: the id is the fallback */ }
|
|
2096
|
+
return id;
|
|
2097
|
+
};
|
|
2098
|
+
// Which hits may be OFFERED. The host index covers every persisted session on
|
|
2099
|
+
// disk — including archived ones and sessions of workspaces this profile no
|
|
2100
|
+
// longer lists — so an unfiltered list offers rows that cannot be opened. The
|
|
2101
|
+
// rule below is the sidebar's own content-search rule (ui-workspace
|
|
2102
|
+
// `sessionVisible` + the registry archive set): listed, not a subagent child,
|
|
2103
|
+
// not archived, and never a blank session unless it is the current one.
|
|
2104
|
+
var crossHitAllowed = function (id) {
|
|
2105
|
+
if (typeof id !== "string" || id === "") return false;
|
|
2106
|
+
var sessions = hostSessions();
|
|
2107
|
+
var snap = null;
|
|
2108
|
+
try {
|
|
2109
|
+
snap = sessions && sessions.list && typeof sessions.list.getSnapshot === "function"
|
|
2110
|
+
? sessions.list.getSnapshot() : null;
|
|
2111
|
+
} catch (e) { snap = null; }
|
|
2112
|
+
var rec = snap && snap.byId ? snap.byId[id] : null;
|
|
2113
|
+
if (rec === undefined || rec === null) return false; // not listed: cannot be opened
|
|
2114
|
+
if (rec.origin === "subagent") return false;
|
|
2115
|
+
if (snap.current === id) return true; // the open session always works
|
|
2116
|
+
if (rec.blank === true) return false; // nothing in it to have matched
|
|
2117
|
+
var archived = null;
|
|
2118
|
+
try {
|
|
2119
|
+
var ws = tocHost && typeof tocHost.workspaces === "function" ? tocHost.workspaces() : null;
|
|
2120
|
+
var wsnap = ws && ws.list && typeof ws.list.getSnapshot === "function" ? ws.list.getSnapshot() : null;
|
|
2121
|
+
archived = wsnap && Array.isArray(wsnap.archivedSessionIds) ? wsnap.archivedSessionIds : null;
|
|
2122
|
+
} catch (e) { archived = null; }
|
|
2123
|
+
// No workspace registry (older host): fall back to the session list's word,
|
|
2124
|
+
// which is still far better than offering every indexed session on disk.
|
|
2125
|
+
if (archived && archived.indexOf(id) >= 0) return false;
|
|
2126
|
+
return true;
|
|
2127
|
+
};
|
|
2128
|
+
|
|
1629
2129
|
// ---- preference plumbing -------------------------------------------------
|
|
1630
2130
|
// The store is the shared source (the native Settings section writes into it);
|
|
1631
2131
|
// the states above are the panel's LIVE copy, so a drag or a toggle never waits
|
|
@@ -1723,7 +2223,11 @@ window.__ModuleLoader__.load({
|
|
|
1723
2223
|
return undefined;
|
|
1724
2224
|
}, [searchAnim]);
|
|
1725
2225
|
react.useEffect(function () {
|
|
1726
|
-
if (searchOpen
|
|
2226
|
+
if (!searchOpen || !searchRef.current) return;
|
|
2227
|
+
// preventScroll: across the curtain the input arrives inside a column that is
|
|
2228
|
+
// still animating open, and focusing it must not scroll anything
|
|
2229
|
+
try { searchRef.current.focus({ preventScroll: true }); }
|
|
2230
|
+
catch (e) { searchRef.current.focus(); }
|
|
1727
2231
|
}, [searchOpen]);
|
|
1728
2232
|
var onQueryChange = function (v) {
|
|
1729
2233
|
setQuery(v);
|
|
@@ -2102,6 +2606,15 @@ window.__ModuleLoader__.load({
|
|
|
2102
2606
|
didInitScroll.current = true;
|
|
2103
2607
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
2104
2608
|
}, [groups.length]);
|
|
2609
|
+
// A live handle on the newest group list, for callbacks that outlive their
|
|
2610
|
+
// render (the "did that turn actually page in?" verdict below).
|
|
2611
|
+
var groupsLiveRef = react.useRef(groups);
|
|
2612
|
+
groupsLiveRef.current = groups;
|
|
2613
|
+
// One history-rebuild retry per session after a jump that never landed.
|
|
2614
|
+
// Hooks must sit ABOVE every early return (this panel returns null for an
|
|
2615
|
+
// empty conversation) or React aborts with #310.
|
|
2616
|
+
var jumpRetryRef = react.useRef(false);
|
|
2617
|
+
react.useEffect(function () { jumpRetryRef.current = false; }, [sessionId]);
|
|
2105
2618
|
// The "back to the newest row" button follows the list's real geometry, and a
|
|
2106
2619
|
// bottom can be reached WITHOUT a scroll event: older turns page in, a search
|
|
2107
2620
|
// replaces the rows, the panel is resized, the panel is expanded/collapsed.
|
|
@@ -2208,6 +2721,9 @@ window.__ModuleLoader__.load({
|
|
|
2208
2721
|
var resultRows = react.useMemo(function () {
|
|
2209
2722
|
var fq = foldQuery(query);
|
|
2210
2723
|
if (!fq) return [];
|
|
2724
|
+
// cross-session scope lists HOST hits instead of local ones: the local list
|
|
2725
|
+
// (and the in-chat highlight it drives) stays out of the way.
|
|
2726
|
+
if (searchScope === "cross") return [];
|
|
2211
2727
|
var needle = { folded: fq }; // one folded query, reused for every text
|
|
2212
2728
|
var rows = [];
|
|
2213
2729
|
var index = {};
|
|
@@ -2334,6 +2850,10 @@ window.__ModuleLoader__.load({
|
|
|
2334
2850
|
activeSigRef.current = sig;
|
|
2335
2851
|
setActiveGroup(actives);
|
|
2336
2852
|
if (actives.length === 0) return;
|
|
2853
|
+
// remember where the reader is (debounced): the topmost visible turn, with
|
|
2854
|
+
// its start seq so a later restore can page it back in
|
|
2855
|
+
var giSave = actives[0];
|
|
2856
|
+
if (groups[giSave]) savePosition(groups[giSave]);
|
|
2337
2857
|
// pause right after the reader touched the outline themselves —
|
|
2338
2858
|
// otherwise paging older turns gets yanked back to the newest group
|
|
2339
2859
|
if (Date.now() - outlineTouchRef.current <= 1500) return;
|
|
@@ -2377,6 +2897,9 @@ window.__ModuleLoader__.load({
|
|
|
2377
2897
|
var matches = react.useMemo(function () {
|
|
2378
2898
|
var fq = foldQuery(query);
|
|
2379
2899
|
if (!fq) return [];
|
|
2900
|
+
// cross-session scope: the host owns the searching, so there are no local
|
|
2901
|
+
// matches — no n/N stepping and no keyword highlight in this conversation.
|
|
2902
|
+
if (searchScope === "cross") return [];
|
|
2380
2903
|
var needle = { folded: fq };
|
|
2381
2904
|
var out = [];
|
|
2382
2905
|
for (var gi = 0; gi < groups.length; gi++) {
|
|
@@ -2429,6 +2952,142 @@ window.__ModuleLoader__.load({
|
|
|
2429
2952
|
}
|
|
2430
2953
|
}
|
|
2431
2954
|
|
|
2955
|
+
// ---- cross-session query: debounce, abort the superseded one ------------
|
|
2956
|
+
// 320ms of quiet keeps a fast typist from spending the host's search budget
|
|
2957
|
+
// on prefixes; the previous request is aborted, and an aborted reply is
|
|
2958
|
+
// dropped (a late answer for an older query must not overwrite the new one).
|
|
2959
|
+
react.useEffect(function () {
|
|
2960
|
+
var q = query.trim();
|
|
2961
|
+
var clearPending = function () {
|
|
2962
|
+
if (crossTimerRef.current) { clearTimeout(crossTimerRef.current); crossTimerRef.current = null; }
|
|
2963
|
+
};
|
|
2964
|
+
if (searchScope !== "cross" || q === "") {
|
|
2965
|
+
if (crossAbortRef.current) { crossAbortRef.current.abort(); crossAbortRef.current = null; }
|
|
2966
|
+
clearPending();
|
|
2967
|
+
setCross(function (cur) {
|
|
2968
|
+
return (cur.phase === "idle" && cur.rows.length === 0) ? cur : { phase: "idle", rows: [], more: false, error: "" };
|
|
2969
|
+
});
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
var sessions = hostSessions();
|
|
2973
|
+
if (!sessions || typeof sessions.search !== "function") {
|
|
2974
|
+
setCross({ phase: "error", rows: [], more: false, error: T("search.cross.unavailable") });
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
clearPending();
|
|
2978
|
+
crossTimerRef.current = setTimeout(function () {
|
|
2979
|
+
crossTimerRef.current = null;
|
|
2980
|
+
if (crossAbortRef.current) crossAbortRef.current.abort();
|
|
2981
|
+
var ctl = (typeof AbortController !== "undefined") ? new AbortController() : null;
|
|
2982
|
+
crossAbortRef.current = ctl;
|
|
2983
|
+
setCross({ phase: "loading", rows: [], more: false, error: "" });
|
|
2984
|
+
var started;
|
|
2985
|
+
try {
|
|
2986
|
+
started = sessions.search(q, ctl ? ctl.signal : undefined);
|
|
2987
|
+
} catch (e) {
|
|
2988
|
+
setCross({ phase: "error", rows: [], more: false, error: String(e && e.message ? e.message : e) });
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
Promise.resolve(started).then(function (result) {
|
|
2992
|
+
if (ctl && ctl.signal.aborted) return;
|
|
2993
|
+
if (!result || result.ok !== true) {
|
|
2994
|
+
var msg = (result && result.error && result.error.message) ? String(result.error.message) : "unknown error";
|
|
2995
|
+
// The deployment default is an index that is never opened, and the host
|
|
2996
|
+
// says so in engineering terms. Say what it means for the reader instead,
|
|
2997
|
+
// and keep the host's own words available for the "any other error" case.
|
|
2998
|
+
if (/SESSION_QUERY_SEARCH_DISABLED|session search is disabled|openAt/i.test(msg)) {
|
|
2999
|
+
msg = T("search.cross.disabled");
|
|
3000
|
+
}
|
|
3001
|
+
setCross({ phase: "error", rows: [], more: false, error: msg });
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
var value = result.value || {};
|
|
3005
|
+
var items = Array.isArray(value.items) ? value.items : [];
|
|
3006
|
+
var rows = [];
|
|
3007
|
+
var hidden = 0;
|
|
3008
|
+
for (var i = 0; i < items.length; i++) {
|
|
3009
|
+
var it = items[i];
|
|
3010
|
+
if (!it || typeof it.sessionId !== "string") continue;
|
|
3011
|
+
// never offer a hit the panel could not open (archived, unlisted,
|
|
3012
|
+
// subagent child): the index covers every persisted session on disk
|
|
3013
|
+
if (!crossHitAllowed(it.sessionId)) { hidden += 1; continue; }
|
|
3014
|
+
rows.push({ sessionId: it.sessionId, snippet: typeof it.snippet === "string" ? it.snippet : "" });
|
|
3015
|
+
}
|
|
3016
|
+
setCross({ phase: "done", rows: rows, more: value.hasMore === true, error: "", hidden: hidden });
|
|
3017
|
+
}, function (e) {
|
|
3018
|
+
if (ctl && ctl.signal.aborted) return;
|
|
3019
|
+
setCross({ phase: "error", rows: [], more: false, error: String(e && e.message ? e.message : e) });
|
|
3020
|
+
});
|
|
3021
|
+
}, 320);
|
|
3022
|
+
}, [searchScope, query, langKey]);
|
|
3023
|
+
|
|
3024
|
+
// leaving the panel unmounts it: abort an in-flight host search
|
|
3025
|
+
react.useEffect(function () {
|
|
3026
|
+
return function () {
|
|
3027
|
+
if (crossAbortRef.current) crossAbortRef.current.abort();
|
|
3028
|
+
if (crossTimerRef.current) clearTimeout(crossTimerRef.current);
|
|
3029
|
+
};
|
|
3030
|
+
}, []);
|
|
3031
|
+
|
|
3032
|
+
// clicking a cross-session hit: switch the session, then leave the keyword for
|
|
3033
|
+
// the panel that comes up (see pendingLocate) — the host hit has no seq, so the
|
|
3034
|
+
// jump happens on the other side, after the same search runs there.
|
|
3035
|
+
var openCrossHit = function (hitSessionId) {
|
|
3036
|
+
var sessions = hostSessions();
|
|
3037
|
+
if (!sessions || typeof sessions.open !== "function") {
|
|
3038
|
+
showBanner(T("search.cross.unavailable"));
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
// the list/archive set can change between the search and the click
|
|
3042
|
+
if (!crossHitAllowed(hitSessionId)) {
|
|
3043
|
+
showBanner(T("search.cross.goneHit"));
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
pendingLocate = { sessionId: hitSessionId, query: query.trim(), at: Date.now() };
|
|
3047
|
+
try {
|
|
3048
|
+
sessions.open(hitSessionId);
|
|
3049
|
+
} catch (e) {
|
|
3050
|
+
pendingLocate = null;
|
|
3051
|
+
showBanner(T("search.cross.error") + String(e && e.message ? e.message : e));
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
showBanner(T("search.cross.switched"));
|
|
3055
|
+
};
|
|
3056
|
+
|
|
3057
|
+
// the arriving side of that hand-off: prefill the keyword in full-text scope,
|
|
3058
|
+
// then jump to the first row that really contains it. Best effort by design —
|
|
3059
|
+
// the spot may sit in history this window has not paged in, so a grace period
|
|
3060
|
+
// ends in an honest "not loaded yet" banner rather than a silent nothing.
|
|
3061
|
+
var locateTimerRef = react.useRef(null);
|
|
3062
|
+
react.useEffect(function () {
|
|
3063
|
+
var req = pendingLocate;
|
|
3064
|
+
if (!req) return;
|
|
3065
|
+
if (req.sessionId !== sessionId || !req.query) return;
|
|
3066
|
+
if (Date.now() - req.at > 15000) { pendingLocate = null; return; }
|
|
3067
|
+
if (query !== req.query) { setQuery(req.query); setSearchScope("full"); return; }
|
|
3068
|
+
if (searchScope !== "full") { setSearchScope("full"); return; }
|
|
3069
|
+
if (resultRows.length === 0) {
|
|
3070
|
+
if (!locateTimerRef.current) {
|
|
3071
|
+
locateTimerRef.current = setTimeout(function () {
|
|
3072
|
+
locateTimerRef.current = null;
|
|
3073
|
+
if (pendingLocate === req) {
|
|
3074
|
+
pendingLocate = null;
|
|
3075
|
+
showBanner(T("search.cross.noSpot"));
|
|
3076
|
+
}
|
|
3077
|
+
}, 2600);
|
|
3078
|
+
}
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
if (locateTimerRef.current) { clearTimeout(locateTimerRef.current); locateTimerRef.current = null; }
|
|
3082
|
+
pendingLocate = null;
|
|
3083
|
+
var pick = 0;
|
|
3084
|
+
for (var i = 0; i < resultRows.length; i++) {
|
|
3085
|
+
var r = resultRows[i];
|
|
3086
|
+
if (((r.title || "") + " " + (r.snippet || "")).indexOf(req.query) >= 0) { pick = i; break; }
|
|
3087
|
+
}
|
|
3088
|
+
goToRow(pick);
|
|
3089
|
+
}, [sessionId, query, searchScope, resultRows]);
|
|
3090
|
+
|
|
2432
3091
|
// The result list follows the outline's convention: OLDEST hit at the top,
|
|
2433
3092
|
// NEWEST at the bottom, and a fresh search starts scrolled to the bottom —
|
|
2434
3093
|
// scroll UP from there to walk back through earlier hits. The newest hit is
|
|
@@ -2442,11 +3101,14 @@ window.__ModuleLoader__.load({
|
|
|
2442
3101
|
if (!q) { setHint(""); return; }
|
|
2443
3102
|
var el = listRef.current;
|
|
2444
3103
|
if (el) el.scrollTop = el.scrollHeight;
|
|
3104
|
+
// cross-session scope: the list is host hits, so neither the n/N cursor nor
|
|
3105
|
+
// the "scroll up for older messages" hint applies
|
|
3106
|
+
if (searchScope === "cross") { setHint(""); return; }
|
|
2445
3107
|
if (matches.length > 0) setMatchIdx(matches.length - 1);
|
|
2446
3108
|
// arm the "scroll up for older messages" hint: the result list only covers
|
|
2447
3109
|
// what the window holds, and older turns are one upward scroll away
|
|
2448
3110
|
setHint(canLoadOlder() ? "more" : "");
|
|
2449
|
-
}, [query, matches.length]);
|
|
3111
|
+
}, [query, matches.length, searchScope]);
|
|
2450
3112
|
|
|
2451
3113
|
// keep the outline filled: after opening the panel or dragging it taller,
|
|
2452
3114
|
// the latest page may be shorter than the list viewport, leaving dead space
|
|
@@ -2515,6 +3177,130 @@ window.__ModuleLoader__.load({
|
|
|
2515
3177
|
return function () { el.removeEventListener("wheel", onWheel); };
|
|
2516
3178
|
}, [open, crowded, groups.length]);
|
|
2517
3179
|
|
|
3180
|
+
// ---- remember the reading position --------------------------------------
|
|
3181
|
+
// Saved: the turn the reader is actually looking at (the first group crossing
|
|
3182
|
+
// the conversation viewport, the same signal the rail follows), written 700ms
|
|
3183
|
+
// after the scrolling stops. Restored: once per session mount, quietly — a
|
|
3184
|
+
// position that can no longer be paged in is forgotten rather than announced.
|
|
3185
|
+
var restoreDoneRef = react.useRef(false);
|
|
3186
|
+
var restoreQuietRef = react.useRef(0);
|
|
3187
|
+
var posTimerRef = react.useRef(null);
|
|
3188
|
+
var rememberRef = react.useRef(true);
|
|
3189
|
+
rememberRef.current = !!prefs.remember;
|
|
3190
|
+
react.useEffect(function () { return function () { if (posTimerRef.current) clearTimeout(posTimerRef.current); }; }, []);
|
|
3191
|
+
// `g` is the outline group the reader is looking at (its turn AND its start seq,
|
|
3192
|
+
// so the position can be paged back in later, not just jumped to while loaded)
|
|
3193
|
+
var savePosition = function (g) {
|
|
3194
|
+
if (!rememberRef.current) return;
|
|
3195
|
+
if (!g || typeof g.turn !== "number") return;
|
|
3196
|
+
if (Date.now() - restoreQuietRef.current < 2500) return; // the restore's own scrolling
|
|
3197
|
+
if (posTimerRef.current) clearTimeout(posTimerRef.current);
|
|
3198
|
+
posTimerRef.current = setTimeout(function () {
|
|
3199
|
+
posTimerRef.current = null;
|
|
3200
|
+
writeReadPos(sessionId, g.turn, g.seq);
|
|
3201
|
+
}, 700);
|
|
3202
|
+
};
|
|
3203
|
+
|
|
3204
|
+
// ---- keyboard cursor: paint + keep in view ------------------------------
|
|
3205
|
+
// Hooks MUST sit above the early return below (a conditional hook aborts the
|
|
3206
|
+
// whole panel with React #310); the helpers they call are defined later, which
|
|
3207
|
+
// is fine because the callbacks run after the render.
|
|
3208
|
+
react.useEffect(function () {
|
|
3209
|
+
var el = listRef.current;
|
|
3210
|
+
var rows = el && typeof el.querySelectorAll === "function"
|
|
3211
|
+
? Array.prototype.slice.call(el.querySelectorAll("[data-nav-row]")) : [];
|
|
3212
|
+
if (navIdx >= rows.length) { setNavIdx(rows.length > 0 ? rows.length - 1 : -1); return; }
|
|
3213
|
+
for (var i = 0; i < rows.length; i++) {
|
|
3214
|
+
if (rows[i] && rows[i].classList && typeof rows[i].classList.toggle === "function") {
|
|
3215
|
+
rows[i].classList.toggle("dqt-nav", i === navIdx);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (navIdx >= 0 && navIdx < rows.length && el && typeof el.getBoundingClientRect === "function"
|
|
3219
|
+
&& typeof rows[navIdx].getBoundingClientRect === "function") {
|
|
3220
|
+
var box = el.getBoundingClientRect();
|
|
3221
|
+
var rr = rows[navIdx].getBoundingClientRect();
|
|
3222
|
+
if (box && rr && typeof box.top === "number" && typeof rr.top === "number") {
|
|
3223
|
+
var factor = zoom > 0 ? zoom : 1;
|
|
3224
|
+
if (rr.top < box.top + 2) el.scrollTop -= (box.top + 2 - rr.top) / factor;
|
|
3225
|
+
else if (rr.bottom > box.bottom - 2) el.scrollTop += (rr.bottom - box.bottom + 2) / factor;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}, [navIdx, groups, resultRows, cross, query, visibleCount, open, questionsOnly]);
|
|
3229
|
+
// live region: what the reader just did (search hits / results / cursor)
|
|
3230
|
+
react.useEffect(function () {
|
|
3231
|
+
if (!query.trim()) { setLiveText(""); return; }
|
|
3232
|
+
if (searchScope === "cross") {
|
|
3233
|
+
if (cross.phase === "loading") setLiveText(T("a11y.loading"));
|
|
3234
|
+
else if (cross.phase === "error") setLiveText(cross.error || T("a11y.crossEmpty"));
|
|
3235
|
+
else setLiveText(cross.rows.length > 0 ? Tp("a11y.results", { n: cross.rows.length }) : T("a11y.crossEmpty"));
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
if (matches.length > 0) setLiveText(Tp("a11y.hits", { n: (matchIdx % matches.length) + 1, total: matches.length }));
|
|
3239
|
+
else setLiveText(T("search.empty"));
|
|
3240
|
+
}, [query, searchScope, cross.phase, cross.rows.length, cross.error, matches.length, matchIdx, langKey]);
|
|
3241
|
+
|
|
3242
|
+
// ---- restore the remembered position (once per session mount) -----------
|
|
3243
|
+
// Hooks must stay above the early return below. Runs after the first content
|
|
3244
|
+
// arrives; a stored turn that is already the newest (or within the newest two
|
|
3245
|
+
// groups) means there is nothing to restore, and a position older than
|
|
3246
|
+
// READ_POS_TTL_MS is dropped instead of resumed: coming back later, the reader
|
|
3247
|
+
// expects the newest messages, not an anchor from a previous sitting.
|
|
3248
|
+
react.useEffect(function () {
|
|
3249
|
+
if (restoreDoneRef.current) return;
|
|
3250
|
+
if (groups.length === 0) return;
|
|
3251
|
+
if (!chatViewActive) return;
|
|
3252
|
+
restoreDoneRef.current = true;
|
|
3253
|
+
if (!prefs.remember) return;
|
|
3254
|
+
var saved = readPos(sessionId);
|
|
3255
|
+
if (!saved) return;
|
|
3256
|
+
if (typeof saved.at === "number" && Date.now() - saved.at > READ_POS_TTL_MS) {
|
|
3257
|
+
clearReadPos(sessionId); // stale: open at the bottom, as if it were never saved
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
var newest = [];
|
|
3261
|
+
for (var i = Math.max(0, groups.length - 2); i < groups.length; i++) {
|
|
3262
|
+
if (typeof groups[i].turn === "number") newest.push(groups[i].turn);
|
|
3263
|
+
}
|
|
3264
|
+
if (newest.indexOf(saved.turn) >= 0) return;
|
|
3265
|
+
restoreQuietRef.current = Date.now();
|
|
3266
|
+
var gi = -1;
|
|
3267
|
+
for (var j = 0; j < groups.length; j++) {
|
|
3268
|
+
if (groups[j].turn === saved.turn) { gi = j; break; }
|
|
3269
|
+
}
|
|
3270
|
+
if (gi >= 0 && !groups[gi].ghost) {
|
|
3271
|
+
var g = groups[gi];
|
|
3272
|
+
var key = (g.msgs && g.msgs.length > 0) ? g.msgs[0].key
|
|
3273
|
+
: (g.userKey || (g.headings.length > 0 ? g.headings[0].key : ""));
|
|
3274
|
+
if (key) {
|
|
3275
|
+
jump(key, 0);
|
|
3276
|
+
revealGroupInOutline(gi);
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
// Not in the loaded window (or nothing jumpable in it): page it in quietly.
|
|
3281
|
+
// The seq comes from the host outline — the stored entry may predate the seq
|
|
3282
|
+
// being written, and loadThrough(undefined) pages nowhere, so the restore used
|
|
3283
|
+
// to sit out its 8s deadline and then forget a position it could have resumed.
|
|
3284
|
+
var seq = typeof saved.seq === "number" ? saved.seq : null;
|
|
3285
|
+
if (seq === null) {
|
|
3286
|
+
for (var k = 0; k < groups.length; k++) {
|
|
3287
|
+
if (groups[k] && groups[k].turn === saved.turn && typeof groups[k].seq === "number") { seq = groups[k].seq; break; }
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
openTurn(saved.turn, seq === null ? undefined : seq, true);
|
|
3291
|
+
}, [groups, prefs.remember, chatViewActive, sessionId]);
|
|
3292
|
+
|
|
3293
|
+
// ---- opening the sheet: land on the turn being read ----------------------
|
|
3294
|
+
// Hooks stay above the early return; the helpers they call are assigned later
|
|
3295
|
+
// in the render body, which is fine because this runs after the render.
|
|
3296
|
+
react.useEffect(function () {
|
|
3297
|
+
if (!sheetOpen) return;
|
|
3298
|
+
var el = listRef.current;
|
|
3299
|
+
if (el && typeof el.focus === "function") el.focus();
|
|
3300
|
+
var gi = activeGroup && activeGroup.length > 0 ? activeGroup[0] : -1;
|
|
3301
|
+
if (gi >= 0) revealGroupInOutline(gi);
|
|
3302
|
+
}, [sheetOpen]);
|
|
3303
|
+
|
|
2518
3304
|
// ---- nothing to show without headings ----
|
|
2519
3305
|
if (groups.length === 0) return null;
|
|
2520
3306
|
|
|
@@ -2522,6 +3308,21 @@ window.__ModuleLoader__.load({
|
|
|
2522
3308
|
// (The scale preference leaves all of this alone: it acts on the panel's inner
|
|
2523
3309
|
// box, so the panel itself keeps the width and height the reader dragged.)
|
|
2524
3310
|
var dockRight = dock === "right";
|
|
3311
|
+
// Keep the OPEN panel inside the window whatever the conversation area does.
|
|
3312
|
+
// The area is the host's to move: its transcript width grips rewrite the
|
|
3313
|
+
// column, a sidebar or another plugin's split view can shift or shrink it,
|
|
3314
|
+
// and the reader can also change the window itself. A panel pushed past the
|
|
3315
|
+
// edge takes its own header — and with it the only ✕ — out of reach, which
|
|
3316
|
+
// is the difference between "misplaced" and "cannot be closed any more".
|
|
3317
|
+
// (The collapsed panel is parked deliberately outside the area and is hidden
|
|
3318
|
+
// by its own clip, so only the open position is clamped.)
|
|
3319
|
+
var clampLeft = function (x) {
|
|
3320
|
+
return Math.max(8, Math.min(Math.max(8, window.innerWidth - panelW - 8), x));
|
|
3321
|
+
};
|
|
3322
|
+
// While the panel is being handed over to (or back from) the curtain it is
|
|
3323
|
+
// rendered at its COLLAPSED geometry, so the same left/clip transition that
|
|
3324
|
+
// serves open/collapse also serves the handoff: no teleport, no flash.
|
|
3325
|
+
var geoOpen = open && !panelAside;
|
|
2525
3326
|
var panelLeft;
|
|
2526
3327
|
if (dockRight) {
|
|
2527
3328
|
// RIGHT dock: keep the panel's VISIBLE right edge pinned at the SAME
|
|
@@ -2531,15 +3332,20 @@ window.__ModuleLoader__.load({
|
|
|
2531
3332
|
// so the visible right edge stays at openRight+panelW -> the
|
|
2532
3333
|
// collapse shrinks toward that line and vanishes exactly there.
|
|
2533
3334
|
var openRight = window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW;
|
|
2534
|
-
panelLeft =
|
|
2535
|
-
} else if (
|
|
3335
|
+
panelLeft = geoOpen ? clampLeft(openRight) : (openRight + panelW);
|
|
3336
|
+
} else if (geoOpen) {
|
|
2536
3337
|
// LEFT dock open (left dock is the confirmed-correct reference — untouched)
|
|
2537
|
-
panelLeft = (viewport ? viewport.left + 8 : 8);
|
|
3338
|
+
panelLeft = clampLeft(viewport ? viewport.left + 8 : 8);
|
|
2538
3339
|
} else {
|
|
2539
3340
|
panelLeft = (viewport ? viewport.left - panelW - 8 : -(panelW + 48));
|
|
2540
3341
|
}
|
|
2541
3342
|
var vMaxH = viewport ? Math.max(200, viewport.height - 28) : "72vh";
|
|
2542
|
-
|
|
3343
|
+
// same reasoning vertically: the draggable offset is stored in localStorage
|
|
3344
|
+
// and can outlive the layout it was chosen for
|
|
3345
|
+
var baseTopPx = Math.max(8, Math.min(
|
|
3346
|
+
Math.max(8, window.innerHeight - 120),
|
|
3347
|
+
(viewport ? viewport.top + 14 : window.innerHeight * 0.12) + panelY
|
|
3348
|
+
));
|
|
2543
3349
|
// the collapsed handle's vertical position (a 0..1 ratio of the conversation
|
|
2544
3350
|
// area — 0 pins the handle to the BOTTOM, 1 to the top; 0.5 = centred, which is
|
|
2545
3351
|
// exactly the previous fixed position)
|
|
@@ -2597,10 +3403,12 @@ window.__ModuleLoader__.load({
|
|
|
2597
3403
|
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
2598
3404
|
return Math.max(0, Math.min(t, floor));
|
|
2599
3405
|
};
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
3406
|
+
// The glide machinery is target-agnostic on purpose: the target is a live
|
|
3407
|
+
// function of the scrollport, so the same stick-zone lift, drift re-aim,
|
|
3408
|
+
// stall watchdog and landing re-checks serve both "put this heading at the
|
|
3409
|
+
// top" (jump) and "put this section's end at the visible bottom" (end jump).
|
|
3410
|
+
var glideToTarget = function (sp, getTarget) {
|
|
3411
|
+
var t0 = getTarget(sp);
|
|
2604
3412
|
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
2605
3413
|
if (floor - sp.scrollTop <= 25 && Math.abs(t0 - sp.scrollTop) > 60) {
|
|
2606
3414
|
sp.scrollTop = Math.max(0, floor - 26); // break the stick-to-bottom hold first
|
|
@@ -2626,7 +3434,7 @@ window.__ModuleLoader__.load({
|
|
|
2626
3434
|
var ticks = 0;
|
|
2627
3435
|
var iv = setInterval(function () {
|
|
2628
3436
|
if (mine.done || (ticks += 1) > 5) { clearInterval(iv); return; }
|
|
2629
|
-
var next =
|
|
3437
|
+
var next = getTarget(sp);
|
|
2630
3438
|
if (Math.abs(next - sp.scrollTop) > 2) sp.scrollTop = next;
|
|
2631
3439
|
}, 120);
|
|
2632
3440
|
};
|
|
@@ -2644,7 +3452,7 @@ window.__ModuleLoader__.load({
|
|
|
2644
3452
|
if (mine.done) { if (glide === mine) glide = null; return; }
|
|
2645
3453
|
var k = Math.min(1, (Date.now() - started) / dur);
|
|
2646
3454
|
var eased = 1 - (1 - k) * (1 - k); // quick off the mark, long coast to a stop
|
|
2647
|
-
var live =
|
|
3455
|
+
var live = getTarget(sp); // the target's offset RIGHT NOW
|
|
2648
3456
|
sp.scrollTop = start + (live - start) * eased;
|
|
2649
3457
|
if (k < 1) requestAnimationFrame(frame);
|
|
2650
3458
|
else land();
|
|
@@ -2676,7 +3484,7 @@ window.__ModuleLoader__.load({
|
|
|
2676
3484
|
var stalls = 0;
|
|
2677
3485
|
var watch = setInterval(function () {
|
|
2678
3486
|
if (mine.done || glide !== mine || drawn) { clearInterval(watch); return; }
|
|
2679
|
-
var live =
|
|
3487
|
+
var live = getTarget(sp);
|
|
2680
3488
|
var top = sp.scrollTop;
|
|
2681
3489
|
if (Math.abs(live - aimed) > 40) { aim(live); lastTop = top; lastMove = Date.now(); return; }
|
|
2682
3490
|
if (Math.abs(top - aimed) <= 2) { clearInterval(watch); land(); return; }
|
|
@@ -2690,6 +3498,12 @@ window.__ModuleLoader__.load({
|
|
|
2690
3498
|
}, 120);
|
|
2691
3499
|
};
|
|
2692
3500
|
|
|
3501
|
+
var glideTo = function (el) {
|
|
3502
|
+
var sp = el.closest ? el.closest("[data-conversation-scroll]") : null;
|
|
3503
|
+
if (!sp) return;
|
|
3504
|
+
glideToTarget(sp, function (s) { return scrollTargetOf(el, s); });
|
|
3505
|
+
};
|
|
3506
|
+
|
|
2693
3507
|
var jump = function (key, idx) {
|
|
2694
3508
|
var row = findRow(key);
|
|
2695
3509
|
if (!row) return;
|
|
@@ -2701,6 +3515,84 @@ window.__ModuleLoader__.load({
|
|
|
2701
3515
|
glideTo(el);
|
|
2702
3516
|
};
|
|
2703
3517
|
|
|
3518
|
+
// ---- jump to the END of a section ----------------------------------------
|
|
3519
|
+
// Every outline row and every group header also carries a small "to the
|
|
3520
|
+
// section's end" control at its far right. The landing is the boundary where
|
|
3521
|
+
// that content stops: for a heading the first subsequent heading of the same
|
|
3522
|
+
// or a higher level, never past the end of its own turn; for a group header
|
|
3523
|
+
// the end of the turn. The boundary is set down just above the sticky
|
|
3524
|
+
// composer seat (its height rides in --dsh-composer-height) so the section's
|
|
3525
|
+
// tail is actually on screen instead of hidden underneath the input box.
|
|
3526
|
+
// Everything shares the target-agnostic glide, so a long end jump gets the
|
|
3527
|
+
// same stick-zone lift, drift re-aim and landing re-checks as a normal jump.
|
|
3528
|
+
var endLimitOf = function (el, sp) {
|
|
3529
|
+
var spTop = sp.getBoundingClientRect().top;
|
|
3530
|
+
var elTop = el.getBoundingClientRect().top;
|
|
3531
|
+
// Everything returned here is SCROLLER-relative (viewport offset + scrollTop):
|
|
3532
|
+
// endTargetTop works in scroller coordinates, and mixing the two frames made
|
|
3533
|
+
// the landing miss by exactly one scrollTop — measured in the browser, the
|
|
3534
|
+
// section it was aiming at ended up thousands of pixels BELOW the viewport.
|
|
3535
|
+
var base = sp.scrollTop - spTop;
|
|
3536
|
+
var limit = null; // scroller-relative y where this entry's content stops
|
|
3537
|
+
if (el.tagName && /^H[1-6]$/.test(String(el.tagName))) {
|
|
3538
|
+
var lvl = Number(String(el.tagName).slice(1));
|
|
3539
|
+
var hs = sp.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
|
3540
|
+
for (var i = 0; i < hs.length; i++) {
|
|
3541
|
+
var h = hs[i];
|
|
3542
|
+
if (h === el) continue;
|
|
3543
|
+
if (h.getBoundingClientRect().top <= elTop + 1) continue;
|
|
3544
|
+
if (Number(String(h.tagName).slice(1)) > lvl) continue; // a child, not a boundary
|
|
3545
|
+
limit = h.getBoundingClientRect().top + base;
|
|
3546
|
+
break;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
var turnEl = el.closest ? el.closest("[data-chat-turn]") : null;
|
|
3550
|
+
if (turnEl) {
|
|
3551
|
+
var tb = turnEl.getBoundingClientRect().bottom + base;
|
|
3552
|
+
if (limit === null || tb < limit) limit = tb;
|
|
3553
|
+
}
|
|
3554
|
+
return limit;
|
|
3555
|
+
};
|
|
3556
|
+
var endTargetTop = function (limit, sp) {
|
|
3557
|
+
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
3558
|
+
if (limit === null) return floor; // nothing bounds it: the very end of the content
|
|
3559
|
+
var seat = parseInt(getComputedStyle(sp).getPropertyValue("--dsh-composer-height"), 10) || 0;
|
|
3560
|
+
var t = limit - (sp.clientHeight - seat) + 28;
|
|
3561
|
+
return Math.max(0, Math.min(t, floor));
|
|
3562
|
+
};
|
|
3563
|
+
var endJumpRow = function (key, idx) {
|
|
3564
|
+
var row = findRow(key);
|
|
3565
|
+
if (!row) return;
|
|
3566
|
+
var el = row;
|
|
3567
|
+
if (idx !== undefined && idx !== null) {
|
|
3568
|
+
var hs = row.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
|
3569
|
+
if (hs.length > 0) el = hs[Math.min(idx, hs.length - 1)] || row;
|
|
3570
|
+
}
|
|
3571
|
+
var sp = el.closest ? el.closest("[data-conversation-scroll]") : null;
|
|
3572
|
+
if (!sp) return;
|
|
3573
|
+
glideToTarget(sp, function (s) { return endTargetTop(endLimitOf(el, s), s); });
|
|
3574
|
+
};
|
|
3575
|
+
// A group header's control lands on the end of the WHOLE turn: the bottom of
|
|
3576
|
+
// the turn's LAST message (the failure row for a failed turn), so the reader
|
|
3577
|
+
// ends up at the bottom of the model's reply.
|
|
3578
|
+
// It deliberately does NOT go through the [data-chat-turn] element. Measured in
|
|
3579
|
+
// the browser: that attribute matches a small marker (0-80px tall in the
|
|
3580
|
+
// sessions probed) whose bottom sits thousands of pixels away from the reply it
|
|
3581
|
+
// names — the last group's marker ran 637->717 in the viewport while that
|
|
3582
|
+
// group's message ran 3539->3984, so anchoring to it dropped the reader nowhere
|
|
3583
|
+
// near the group they clicked.
|
|
3584
|
+
var endJumpTurn = function (lastKey) {
|
|
3585
|
+
var row = lastKey ? findRow(lastKey) : null;
|
|
3586
|
+
if (!row) return;
|
|
3587
|
+
var sp = row.closest ? row.closest("[data-conversation-scroll]") : null;
|
|
3588
|
+
if (!sp) return;
|
|
3589
|
+
glideToTarget(sp, function (s) {
|
|
3590
|
+
// scroller-relative (+ scrollTop): re-evaluated on every tick
|
|
3591
|
+
var base = row.getBoundingClientRect().bottom + s.scrollTop - s.getBoundingClientRect().top;
|
|
3592
|
+
return endTargetTop(base, s);
|
|
3593
|
+
});
|
|
3594
|
+
};
|
|
3595
|
+
|
|
2704
3596
|
// ---- unloaded turns: page history to the turn, then land on it ----------
|
|
2705
3597
|
// The host turn outline carries each turn's `turn/start` seq and the session
|
|
2706
3598
|
// face's loadThrough(seq) is the documented jump loader ("page history
|
|
@@ -2748,24 +3640,123 @@ window.__ModuleLoader__.load({
|
|
|
2748
3640
|
};
|
|
2749
3641
|
|
|
2750
3642
|
// returns false when the host exposes no jump loader (older DSH builds)
|
|
2751
|
-
var
|
|
3643
|
+
var turnLanded = function (turn) {
|
|
3644
|
+
var list = groupsLiveRef.current || [];
|
|
3645
|
+
for (var i = 0; i < list.length; i++) {
|
|
3646
|
+
if (list[i].turn === turn && !list[i].ghost) return true;
|
|
3647
|
+
}
|
|
3648
|
+
return false;
|
|
3649
|
+
};
|
|
3650
|
+
var openTurn = function (turn, seq, quiet) {
|
|
2752
3651
|
var sessions = hostSessions();
|
|
2753
3652
|
if (!sessions) {
|
|
2754
3653
|
console.warn("[dsh-quick-toc] ctx.get(\"sessions\") returned nothing: this host has no session service, cannot open turn " + turn);
|
|
3654
|
+
if (!quiet) showBanner(T("turn.loadFailed"));
|
|
2755
3655
|
return false;
|
|
2756
3656
|
}
|
|
2757
3657
|
var binding = typeof sessions.binding === "function" ? sessions.binding(sessionId) : null;
|
|
2758
3658
|
var face = binding && binding.session;
|
|
2759
3659
|
if (!face || typeof face.loadThrough !== "function") {
|
|
2760
3660
|
console.warn("[dsh-quick-toc] session " + String(sessionId) + " exposes no loadThrough jump loader, cannot open turn " + turn);
|
|
3661
|
+
if (!quiet) showBanner(T("turn.loadFailed"));
|
|
2761
3662
|
return false;
|
|
2762
3663
|
}
|
|
3664
|
+
// The loader silently no-ops when the host considers the window complete
|
|
3665
|
+
// (`hasMore === false`) — that is DEFINITIVE evidence this turn can never be
|
|
3666
|
+
// paged in, so it gets an honest banner. Every other state (session still
|
|
3667
|
+
// opening, a page pull in flight) is transient: the ladder below keeps asking.
|
|
3668
|
+
// A host that exposes no page state at all is asked as before.
|
|
3669
|
+
//
|
|
3670
|
+
// Measured in the browser: right after a session is (re)opened the face reports
|
|
3671
|
+
// `openState: "loading"` WITH `hasMore: false` — the history window has simply
|
|
3672
|
+
// not been read yet. Taking that as definitive dropped a perfectly good
|
|
3673
|
+
// remembered position (the restore forgot it and left the reader wherever the
|
|
3674
|
+
// app happened to be), so `hasMore === false` only counts once the face says the
|
|
3675
|
+
// session is actually open.
|
|
3676
|
+
var state = { open: face.openState, more: face.hasMore, base: face.baseSeq };
|
|
3677
|
+
var sessionSettled = face.openState === undefined || face.openState === "open";
|
|
3678
|
+
var cannotPage = face.hasMore === false && sessionSettled;
|
|
3679
|
+
if (cannotPage) {
|
|
3680
|
+
console.warn("[dsh-quick-toc] jump loader cannot page to turn " + turn + " (seq " + seq + "): openState="
|
|
3681
|
+
+ String(state.open) + ", hasMore=" + String(state.more) + ", baseSeq=" + String(state.base)
|
|
3682
|
+
+ " — the outline names more turns than this host will page in");
|
|
3683
|
+
}
|
|
2763
3684
|
setHoverCard(null);
|
|
2764
|
-
|
|
3685
|
+
// picking a row in the sheet hands the reader back to the conversation
|
|
3686
|
+
if (sheetOpen) closeSheet();
|
|
3687
|
+
// immediately while a plain page pull owns the busy flag (`loadingOlder`),
|
|
3688
|
+
// and the caller is expected to retry once it settles. That is exactly
|
|
3689
|
+
// "sometimes one click does nothing and a couple of clicks work" — the
|
|
3690
|
+
// clicks that land during an in-flight page are silently discarded. So ask
|
|
3691
|
+
// again on a short ladder: while the loader still says it can page, until
|
|
3692
|
+
// the turn actually lands or a deadline passes.
|
|
3693
|
+
var landed = false;
|
|
3694
|
+
var landingIssued = false;
|
|
3695
|
+
var finish = function () {
|
|
3696
|
+
if (landed) return;
|
|
3697
|
+
landed = true;
|
|
2765
3698
|
landOnTurn(turn);
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
|
|
3699
|
+
};
|
|
3700
|
+
var giveUp = function () {
|
|
3701
|
+
// a restore is a convenience, not a promise: it never banners, it just
|
|
3702
|
+
// forgets the position so it cannot nag on every open
|
|
3703
|
+
if (quiet) { clearReadPos(sessionId); return; }
|
|
3704
|
+
// last resort, once per session: rebuild the history source and retry
|
|
3705
|
+
if (jumpRetryRef.current) { showBanner(T("turn.loadFailed")); return; }
|
|
3706
|
+
jumpRetryRef.current = true;
|
|
3707
|
+
console.warn("[dsh-quick-toc] turn " + turn + " did not page in; resyncing the history window and retrying once");
|
|
3708
|
+
var rebuilt;
|
|
3709
|
+
try {
|
|
3710
|
+
rebuilt = typeof face.resync === "function" ? face.resync() : null;
|
|
3711
|
+
} catch (e) {
|
|
3712
|
+
rebuilt = Promise.reject(e);
|
|
3713
|
+
}
|
|
3714
|
+
Promise.resolve(rebuilt).then(function () {
|
|
3715
|
+
return face.loadThrough(seq);
|
|
3716
|
+
}).then(function () {
|
|
3717
|
+
landOnTurn(turn);
|
|
3718
|
+
setTimeout(function () {
|
|
3719
|
+
if (!turnLanded(turn)) showBanner(T("turn.loadFailed"));
|
|
3720
|
+
}, 2500);
|
|
3721
|
+
}, function (err) {
|
|
3722
|
+
console.warn("[dsh-quick-toc] resync retry failed:", err);
|
|
3723
|
+
showBanner(T("turn.loadFailed"));
|
|
3724
|
+
});
|
|
3725
|
+
};
|
|
3726
|
+
if (cannotPage) {
|
|
3727
|
+
if (quiet) clearReadPos(sessionId);
|
|
3728
|
+
else showBanner(T("turn.noMoreHistory"));
|
|
3729
|
+
return true;
|
|
3730
|
+
}
|
|
3731
|
+
var deadline = Date.now() + 8000;
|
|
3732
|
+
var ask = function () {
|
|
3733
|
+
if (turnLanded(turn)) { finish(); return; }
|
|
3734
|
+
// same guard as above: a "not open yet" face is asked again rather than
|
|
3735
|
+
// written off (its hasMore is still the placeholder value)
|
|
3736
|
+
var settledNow = face.openState === undefined || face.openState === "open";
|
|
3737
|
+
if (face.hasMore === false && settledNow) {
|
|
3738
|
+
if (quiet) clearReadPos(sessionId);
|
|
3739
|
+
else showBanner(T("turn.noMoreHistory"));
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
if (Date.now() > deadline) { giveUp(); return; }
|
|
3743
|
+
try {
|
|
3744
|
+
Promise.resolve(face.loadThrough(seq)).then(function () {
|
|
3745
|
+
if (turnLanded(turn)) { finish(); return; }
|
|
3746
|
+
// the page is still arriving: let the landing helper start retrying for
|
|
3747
|
+
// the row (it gives up on its own), and keep the ask-ladder running
|
|
3748
|
+
if (!landingIssued) { landingIssued = true; landOnTurn(turn); }
|
|
3749
|
+
}, function (err) {
|
|
3750
|
+
console.warn("[dsh-quick-toc] loadThrough failed:", err);
|
|
3751
|
+
});
|
|
3752
|
+
} catch (e) {
|
|
3753
|
+
console.warn("[dsh-quick-toc] loadThrough threw:", e);
|
|
3754
|
+
}
|
|
3755
|
+
setTimeout(ask, 400);
|
|
3756
|
+
};
|
|
3757
|
+
// first ask immediately (a click must reach the loader in the same task it
|
|
3758
|
+
// happened); the retries ride the ladder
|
|
3759
|
+
ask();
|
|
2769
3760
|
return true;
|
|
2770
3761
|
};
|
|
2771
3762
|
|
|
@@ -2835,14 +3826,15 @@ window.__ModuleLoader__.load({
|
|
|
2835
3826
|
};
|
|
2836
3827
|
|
|
2837
3828
|
// ---- circular icon button (glyph optical offset: ox/oy px) ----
|
|
2838
|
-
var iconBtn = function (onClick, tip, glyph, fontSize, offset) {
|
|
3829
|
+
var iconBtn = function (onClick, tip, glyph, fontSize, offset, size) {
|
|
2839
3830
|
var ox = offset ? (offset.x || 0) : 0;
|
|
2840
3831
|
var oy = offset ? (offset.y || 0) : 0;
|
|
3832
|
+
var box = size || 24;
|
|
2841
3833
|
return react_jsx_runtime.jsx("button", {
|
|
2842
3834
|
onClick: onClick,
|
|
2843
3835
|
style: {
|
|
2844
|
-
width:
|
|
2845
|
-
height:
|
|
3836
|
+
width: box,
|
|
3837
|
+
height: box,
|
|
2846
3838
|
display: "flex",
|
|
2847
3839
|
alignItems: "center",
|
|
2848
3840
|
justifyContent: "center",
|
|
@@ -2885,8 +3877,133 @@ window.__ModuleLoader__.load({
|
|
|
2885
3877
|
fuzzy: fuzzy,
|
|
2886
3878
|
onOpenTurn: openTurn,
|
|
2887
3879
|
hoverStart: hoverStart,
|
|
2888
|
-
hoverEnd: hoverEnd
|
|
3880
|
+
hoverEnd: hoverEnd,
|
|
3881
|
+
// the curtain is wide: rows switch to a one-line, multi-part layout that
|
|
3882
|
+
// puts the section's opening text out to the right instead of under the
|
|
3883
|
+
// title (see renderItem / renderResultRow)
|
|
3884
|
+
wide: sheetOpen
|
|
3885
|
+
};
|
|
3886
|
+
|
|
3887
|
+
// ---- keyboard cursor plumbing -------------------------------------------
|
|
3888
|
+
// Rows are collected from the DOM in document order, so the cursor sequence is
|
|
3889
|
+
// exactly what the reader sees — one rule for the outline, the questions-only
|
|
3890
|
+
// list, the in-session results and the cross-session results alike.
|
|
3891
|
+
var navRows = function () {
|
|
3892
|
+
var el = listRef.current;
|
|
3893
|
+
if (!el || typeof el.querySelectorAll !== "function") return [];
|
|
3894
|
+
var found = el.querySelectorAll("[data-nav-row]");
|
|
3895
|
+
return Array.prototype.slice.call(found);
|
|
3896
|
+
};
|
|
3897
|
+
var markNav = function (el, on) {
|
|
3898
|
+
if (!el) return;
|
|
3899
|
+
if (el.classList && typeof el.classList.toggle === "function") {
|
|
3900
|
+
el.classList.toggle("dqt-nav", on);
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
var cls = String(el.className || "").split(/\s+/).filter(function (c) { return c && c !== "dqt-nav"; });
|
|
3904
|
+
if (on) cls.push("dqt-nav");
|
|
3905
|
+
el.className = cls.join(" ");
|
|
2889
3906
|
};
|
|
3907
|
+
var rowText = function (el) {
|
|
3908
|
+
if (!el) return "";
|
|
3909
|
+
var text = String(el.textContent || "").replace(/\s+/g, " ").trim();
|
|
3910
|
+
return text.length > 60 ? text.slice(0, 60) + "…" : text;
|
|
3911
|
+
};
|
|
3912
|
+
var scrollRowIntoView = function (el, row) {
|
|
3913
|
+
if (!el || !row || typeof row.getBoundingClientRect !== "function") return;
|
|
3914
|
+
var box = el.getBoundingClientRect();
|
|
3915
|
+
var rr = row.getBoundingClientRect();
|
|
3916
|
+
if (!box || !rr || typeof box.top !== "number") return;
|
|
3917
|
+
// getBoundingClientRect reports VISUAL pixels while scrollTop is layout: the
|
|
3918
|
+
// panel's zoom factor divides the delta (same reasoning as zLen).
|
|
3919
|
+
var factor = zoom > 0 ? zoom : 1;
|
|
3920
|
+
if (rr.top < box.top + 2) el.scrollTop -= (box.top + 2 - rr.top) / factor;
|
|
3921
|
+
else if (rr.bottom > box.bottom - 2) el.scrollTop += (rr.bottom - box.bottom + 2) / factor;
|
|
3922
|
+
};
|
|
3923
|
+
// which row is the reader looking at? (used when the cursor engages, so ↓
|
|
3924
|
+
// continues from the current position instead of restarting at the top)
|
|
3925
|
+
var nearestRowIndex = function (rows) {
|
|
3926
|
+
var el = listRef.current;
|
|
3927
|
+
if (!el || rows.length === 0) return 0;
|
|
3928
|
+
var box = el.getBoundingClientRect();
|
|
3929
|
+
var mid = box && typeof box.top === "number" ? (box.top + box.bottom) / 2 : 0;
|
|
3930
|
+
var best = 0;
|
|
3931
|
+
var bestGap = Infinity;
|
|
3932
|
+
for (var i = 0; i < rows.length; i++) {
|
|
3933
|
+
var rr = rows[i].getBoundingClientRect ? rows[i].getBoundingClientRect() : null;
|
|
3934
|
+
if (!rr || typeof rr.top !== "number") continue;
|
|
3935
|
+
var gap = Math.abs((rr.top + rr.bottom) / 2 - mid);
|
|
3936
|
+
if (gap < bestGap) { bestGap = gap; best = i; }
|
|
3937
|
+
}
|
|
3938
|
+
return best;
|
|
3939
|
+
};
|
|
3940
|
+
var moveNav = function (delta) {
|
|
3941
|
+
var rows = navRows();
|
|
3942
|
+
if (rows.length === 0) return;
|
|
3943
|
+
var next;
|
|
3944
|
+
if (navIdx < 0) next = nearestRowIndex(rows);
|
|
3945
|
+
else next = navIdx + delta;
|
|
3946
|
+
if (next < 0) next = 0;
|
|
3947
|
+
if (next > rows.length - 1) next = rows.length - 1;
|
|
3948
|
+
setNavIdx(next);
|
|
3949
|
+
setLiveText(rowText(rows[next]));
|
|
3950
|
+
};
|
|
3951
|
+
var jumpNav = function () {
|
|
3952
|
+
var rows = navRows();
|
|
3953
|
+
if (rows.length === 0) return;
|
|
3954
|
+
var idx = navIdx < 0 ? nearestRowIndex(rows) : navIdx;
|
|
3955
|
+
if (idx < 0 || idx >= rows.length) return;
|
|
3956
|
+
var row = rows[idx];
|
|
3957
|
+
setNavIdx(idx);
|
|
3958
|
+
setLiveText(rowText(row));
|
|
3959
|
+
if (row && typeof row.click === "function") row.click();
|
|
3960
|
+
};
|
|
3961
|
+
// Consume a key without assuming the event object is a full DOM event (the host
|
|
3962
|
+
// and test harnesses can synthesize partial ones).
|
|
3963
|
+
var swallow = function (e) {
|
|
3964
|
+
if (!e) return;
|
|
3965
|
+
if (typeof e.preventDefault === "function") e.preventDefault();
|
|
3966
|
+
if (typeof e.stopPropagation === "function") e.stopPropagation();
|
|
3967
|
+
};
|
|
3968
|
+
var onPanelKeyDown = function (e) {
|
|
3969
|
+
var target = e.target;
|
|
3970
|
+
var tag = target && target.tagName ? String(target.tagName).toUpperCase() : "";
|
|
3971
|
+
// the search box owns its own keys (it stops propagation for the ones it uses)
|
|
3972
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || (target && target.isContentEditable)) return;
|
|
3973
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
3974
|
+
var key = e.key;
|
|
3975
|
+
if (key === "ArrowDown" || key === "ArrowUp") {
|
|
3976
|
+
swallow(e);
|
|
3977
|
+
moveNav(key === "ArrowDown" ? 1 : -1);
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3980
|
+
if (key === "Home" || key === "End") {
|
|
3981
|
+
var rows = navRows();
|
|
3982
|
+
if (rows.length === 0) return;
|
|
3983
|
+
swallow(e);
|
|
3984
|
+
var idx = key === "Home" ? 0 : rows.length - 1;
|
|
3985
|
+
setNavIdx(idx);
|
|
3986
|
+
setLiveText(rowText(rows[idx]));
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
if (key === "Enter") {
|
|
3990
|
+
swallow(e);
|
|
3991
|
+
jumpNav();
|
|
3992
|
+
return;
|
|
3993
|
+
}
|
|
3994
|
+
if (key === "Escape") {
|
|
3995
|
+
if (sheetOpen) { swallow(e); closeSheet(); return; }
|
|
3996
|
+
if (!open) return;
|
|
3997
|
+
swallow(e);
|
|
3998
|
+
setNavIdx(-1);
|
|
3999
|
+
setOpen(false);
|
|
4000
|
+
}
|
|
4001
|
+
};
|
|
4002
|
+
// keep the painted cursor on the row the index points at, and keep that row in
|
|
4003
|
+
// view. Runs whenever the row set itself changes (page-in, new results, a mode
|
|
4004
|
+
// switch) so the cursor never survives onto a different row. (The effect itself
|
|
4005
|
+
// lives above the early return, with the other hooks.)
|
|
4006
|
+
|
|
2890
4007
|
// Transient banner pinned to the bottom of the panel (the panel is
|
|
2891
4008
|
// position:fixed, so this anchors to the list's lower edge without joining
|
|
2892
4009
|
// the list's scroll flow). pointerEvents:none keeps it from eating clicks.
|
|
@@ -2928,9 +4045,13 @@ window.__ModuleLoader__.load({
|
|
|
2928
4045
|
// while `zoom` itself does not animate: leaving `left` in the transition on that
|
|
2929
4046
|
// one render would let the panel slide across the screen. It lands on exactly the
|
|
2930
4047
|
// same visual spot either way, so that render simply skips the transition.
|
|
2931
|
-
|
|
4048
|
+
// The tuck is a departure even though `open` is still true, so it has to move
|
|
4049
|
+
// at the CLOSING speed: with the opening 0.6s curve the panel was still
|
|
4050
|
+
// travelling when the curtain took over, which looked like it flashed away.
|
|
4051
|
+
var panelAway = !open || panelAside === "tuck";
|
|
4052
|
+
var slide = panelAway ? "0.28s " + EASE : "0.6s " + EASE;
|
|
2932
4053
|
var panelTransition = (zoomJustChanged ? "" : "left " + slide + ", ")
|
|
2933
|
-
+ "clip-path " + slide + ", opacity " + (
|
|
4054
|
+
+ "clip-path " + slide + ", opacity " + (panelAway ? "0.14s ease 0.26s" : "0.45s ease");
|
|
2934
4055
|
|
|
2935
4056
|
// clip the panel at the dock edge while collapsed, so sliding away looks
|
|
2936
4057
|
// like being covered by the sidebar (the sidebar stays untouched):
|
|
@@ -2939,7 +4060,7 @@ window.__ModuleLoader__.load({
|
|
|
2939
4060
|
// LEFT dock keeps its original clip (collapse left). RIGHT dock clips the
|
|
2940
4061
|
// LEFT side so the panel closes toward its right (dock) edge in place.
|
|
2941
4062
|
var panelClip = "inset(0 0 0 0px)";
|
|
2942
|
-
if (!
|
|
4063
|
+
if (!geoOpen) {
|
|
2943
4064
|
if (dockRight) {
|
|
2944
4065
|
// mirror of LEFT: clip the RIGHT side (panel flies right, shrinking from its right)
|
|
2945
4066
|
panelClip = "inset(0 " + zLen(panelW) + " 0 0)";
|
|
@@ -2955,277 +4076,415 @@ window.__ModuleLoader__.load({
|
|
|
2955
4076
|
// renderGroups) instead of from fading everything else away.
|
|
2956
4077
|
var panelOpacity = open && chatViewActive ? (hovered ? 1 : 0.72) : 0;
|
|
2957
4078
|
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
e.
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
4079
|
+
// ---- toolbar buttons -----------------------------------------------------
|
|
4080
|
+
// ONE instance of each, placed either in the docked header row or in the
|
|
4081
|
+
// curtain's own chrome. On the curtain they are larger (that surface has the
|
|
4082
|
+
// room), and the dock / curtain / collapse controls are not built at all —
|
|
4083
|
+
// they mean nothing on a panel that already spans the conversation.
|
|
4084
|
+
var tbSize = sheetOpen ? 32 : 24;
|
|
4085
|
+
var tbIcon = sheetOpen ? 18 : 14;
|
|
4086
|
+
var tbIconLg = sheetOpen ? 20 : 15;
|
|
4087
|
+
var toolBtn = function (tool, className, onClick, title, active, disabled, svg, extra) {
|
|
4088
|
+
var props = {
|
|
4089
|
+
className: className,
|
|
4090
|
+
onClick: onClick,
|
|
4091
|
+
title: title,
|
|
4092
|
+
"data-tool": tool,
|
|
4093
|
+
style: {
|
|
4094
|
+
width: tbSize,
|
|
4095
|
+
height: tbSize,
|
|
4096
|
+
padding: 0,
|
|
4097
|
+
border: "none",
|
|
4098
|
+
borderRadius: "50%",
|
|
4099
|
+
cornerShape: "round",
|
|
4100
|
+
flex: "none",
|
|
4101
|
+
display: "flex",
|
|
4102
|
+
alignItems: "center",
|
|
4103
|
+
justifyContent: "center",
|
|
4104
|
+
cursor: disabled ? "default" : "pointer",
|
|
4105
|
+
opacity: disabled ? 0.45 : 1,
|
|
4106
|
+
// open (or still animating shut) uses the SAME tint as every other
|
|
4107
|
+
// "on" control, so the toolbar reads consistently in both layouts
|
|
4108
|
+
background: active ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
4109
|
+
color: active ? C.accent : C.muted,
|
|
4110
|
+
transition: "background 0.15s ease, color 0.15s ease, opacity 0.2s ease"
|
|
4111
|
+
},
|
|
4112
|
+
onMouseEnter: function (e) {
|
|
4113
|
+
if (disabled) return;
|
|
4114
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
4115
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
4116
|
+
},
|
|
4117
|
+
onMouseLeave: function (e) {
|
|
4118
|
+
e.currentTarget.style.background = active ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
4119
|
+
e.currentTarget.style.color = active ? C.accent : C.muted;
|
|
4120
|
+
},
|
|
4121
|
+
children: svg
|
|
4122
|
+
};
|
|
4123
|
+
// always present (false, not undefined): the toolbar reads it to decide the
|
|
4124
|
+
// resting cursor/opacity, and the tests assert the false case explicitly
|
|
4125
|
+
props.disabled = !!disabled;
|
|
4126
|
+
if (extra) {
|
|
4127
|
+
for (var k in extra) {
|
|
4128
|
+
if (Object.prototype.hasOwnProperty.call(extra, k)) props[k] = extra[k];
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
return react_jsx_runtime.jsx("button", props);
|
|
4132
|
+
};
|
|
4133
|
+
// heading-level filter: a round icon button that pops down the H1–H6 picker.
|
|
4134
|
+
// Icon: three lines of decreasing width = outline levels.
|
|
4135
|
+
var levelsBtnEl = toolBtn("levels", "dqt-levels-btn",
|
|
4136
|
+
function () { if (questionsOnly) return; levelsOpen ? closeLevels() : setLevelsOpen(true); },
|
|
4137
|
+
questionsOnly ? T("levels.offInQuestions") : T("levels.tip"),
|
|
4138
|
+
(levelsOpen || levelsClosing), questionsOnly,
|
|
4139
|
+
react_jsx_runtime.jsx("svg", {
|
|
4140
|
+
width: tbIcon,
|
|
4141
|
+
height: tbIcon,
|
|
4142
|
+
viewBox: "0 0 14 14",
|
|
4143
|
+
fill: "none",
|
|
4144
|
+
stroke: "currentColor",
|
|
4145
|
+
strokeWidth: 1.8,
|
|
4146
|
+
strokeLinecap: "round",
|
|
4147
|
+
style: { display: "block" },
|
|
4148
|
+
children: [
|
|
4149
|
+
react_jsx_runtime.jsx("line", { x1: 1.5, y1: 3, x2: 12.5, y2: 3 }),
|
|
4150
|
+
react_jsx_runtime.jsx("line", { x1: 1.5, y1: 7, x2: 9, y2: 7 }),
|
|
4151
|
+
react_jsx_runtime.jsx("line", { x1: 1.5, y1: 11, x2: 5.5, y2: 11 })
|
|
4152
|
+
]
|
|
4153
|
+
})
|
|
4154
|
+
);
|
|
4155
|
+
// questions-only toggle: icon = a speech bubble (the prompt), which is exactly
|
|
4156
|
+
// what that view keeps
|
|
4157
|
+
var questionsBtnEl = toolBtn("questions", "dqt-questions-btn", toggleQuestions,
|
|
4158
|
+
questionsOnly ? T("view.questions.tipOn") : T("view.questions.tipOff"),
|
|
4159
|
+
questionsOnly, false,
|
|
4160
|
+
react_jsx_runtime.jsx("svg", {
|
|
4161
|
+
width: tbIcon,
|
|
4162
|
+
height: tbIcon,
|
|
4163
|
+
viewBox: "0 0 14 14",
|
|
4164
|
+
fill: "none",
|
|
4165
|
+
stroke: "currentColor",
|
|
4166
|
+
strokeWidth: 1.8,
|
|
4167
|
+
strokeLinecap: "round",
|
|
4168
|
+
strokeLinejoin: "round",
|
|
4169
|
+
style: { display: "block" },
|
|
4170
|
+
children: [
|
|
4171
|
+
react_jsx_runtime.jsx("rect", { x: 1.6, y: 2.4, width: 10.8, height: 7.2, rx: 1.6 }),
|
|
4172
|
+
react_jsx_runtime.jsx("path", { d: "M4.6 9.6v2.6l2.8-2.6" })
|
|
4173
|
+
]
|
|
4174
|
+
}),
|
|
4175
|
+
{ "data-questions": questionsOnly ? "on" : "off" }
|
|
4176
|
+
);
|
|
4177
|
+
// magnifier (SVG, matches the other buttons' style)
|
|
4178
|
+
var searchBtnEl = toolBtn("search", "dqt-search-btn",
|
|
4179
|
+
function () { searchOpen ? closeSearch() : openSearch(); },
|
|
4180
|
+
T("search.open"), searchOpen, false,
|
|
4181
|
+
react_jsx_runtime.jsx("svg", {
|
|
4182
|
+
width: tbIconLg,
|
|
4183
|
+
height: tbIconLg,
|
|
4184
|
+
viewBox: "0 0 24 24",
|
|
4185
|
+
// overflow visible: the stroke may extend past the
|
|
4186
|
+
// viewBox without being clipped at the svg boundary
|
|
4187
|
+
style: { display: "block", transform: "translate(-1px, -1px)", overflow: "visible" },
|
|
4188
|
+
fill: "none",
|
|
4189
|
+
stroke: "currentColor",
|
|
4190
|
+
strokeWidth: 3.6,
|
|
4191
|
+
strokeLinecap: "round",
|
|
4192
|
+
children: [
|
|
4193
|
+
react_jsx_runtime.jsx("circle", { cx: 11, cy: 11, r: 7 }),
|
|
4194
|
+
react_jsx_runtime.jsx("line", { x1: 21, y1: 21, x2: 16, y2: 16 })
|
|
4195
|
+
]
|
|
4196
|
+
})
|
|
4197
|
+
);
|
|
4198
|
+
// the curtain's way out: the SAME thick SVG cross as the docked header's
|
|
4199
|
+
// collapse button, scaled with the rest of the curtain's buttons
|
|
4200
|
+
var sheetCloseBtnEl = toolBtn("close", "dqt-sheet-close-btn", closeSheet, T("sheet.close"), false, false,
|
|
4201
|
+
react_jsx_runtime.jsx("svg", {
|
|
4202
|
+
width: tbIconLg,
|
|
4203
|
+
height: tbIconLg,
|
|
4204
|
+
viewBox: "0 0 24 24",
|
|
4205
|
+
style: { display: "block", transform: "translate(1px, 0px)", overflow: "visible" },
|
|
4206
|
+
fill: "none",
|
|
4207
|
+
stroke: "currentColor",
|
|
4208
|
+
strokeWidth: 3.4,
|
|
4209
|
+
strokeLinecap: "round",
|
|
4210
|
+
children: [
|
|
4211
|
+
// cross lines span 5..19, intersection (12,12) centered
|
|
4212
|
+
react_jsx_runtime.jsx("line", { x1: 5, y1: 5, x2: 19, y2: 19 }),
|
|
4213
|
+
react_jsx_runtime.jsx("line", { x1: 19, y1: 5, x2: 5, y2: 19 })
|
|
4214
|
+
]
|
|
4215
|
+
})
|
|
4216
|
+
);
|
|
4217
|
+
// heading-level picker: pops down from the round toolbar button. It is anchored
|
|
4218
|
+
// to the toolbar itself on the curtain (no overflow-clipped row there) and to
|
|
4219
|
+
// the panel box in the docked layout (the header row scrolls sideways, so a
|
|
4220
|
+
// child of it would be clipped). Open/close animate from the button's center;
|
|
4221
|
+
// the animation lives in the injected stylesheet (classes, not inline
|
|
4222
|
+
// animation) so React re-renders never restart or cancel it.
|
|
4223
|
+
var buildLevelsPop = function (inSheet) {
|
|
4224
|
+
// on the curtain the six levels are ONE spread-out row (there is room);
|
|
4225
|
+
// in the narrow docked panel they have to wrap, and they are capped by the
|
|
4226
|
+
// panel's own width rather than by the shrink-to-fit toolbar wrapper
|
|
4227
|
+
var popW = inSheet ? "min(336px, 74vw)" : undefined;
|
|
4228
|
+
return react_jsx_runtime.jsx("div", {
|
|
4229
|
+
className: "dqt-levels-pop" + (levelsClosing ? " dqt-levels-pop-closing" : ""),
|
|
4230
|
+
style: {
|
|
4231
|
+
position: "absolute",
|
|
4232
|
+
top: inSheet ? "calc(100% + 8px)" : 40,
|
|
4233
|
+
left: inSheet ? 0 : 10,
|
|
4234
|
+
zIndex: 30,
|
|
4235
|
+
display: "flex",
|
|
4236
|
+
alignItems: "center",
|
|
4237
|
+
flexWrap: inSheet ? "nowrap" : "wrap",
|
|
4238
|
+
gap: 4,
|
|
4239
|
+
padding: 8,
|
|
4240
|
+
background: C.panelBg,
|
|
4241
|
+
border: "1px solid " + C.panelBorder,
|
|
4242
|
+
borderRadius: 10,
|
|
4243
|
+
cornerShape: "round",
|
|
4244
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.28), 0 2px 6px rgba(0, 0, 0, 0.18)",
|
|
4245
|
+
width: popW,
|
|
4246
|
+
maxWidth: inSheet ? popW : "calc(100% - 20px)",
|
|
4247
|
+
// the button's center sits 12px (dock) / 16px (curtain) inside the popup
|
|
4248
|
+
transformOrigin: (inSheet ? tbSize / 2 : 12) + "px top"
|
|
4249
|
+
},
|
|
4250
|
+
children: [1, 2, 3, 4, 5, 6].map(function (lv) {
|
|
4251
|
+
var active = !!levelSet[lv];
|
|
4252
|
+
return react_jsx_runtime.jsx("button", {
|
|
4253
|
+
onClick: function () { toggleLevel(lv); },
|
|
4254
|
+
title: active ? T("levels.hide") + lv : T("levels.show") + lv,
|
|
4255
|
+
style: {
|
|
4256
|
+
height: inSheet ? 32 : 22,
|
|
4257
|
+
flex: inSheet ? "1 1 0" : "0 0 auto",
|
|
4258
|
+
minWidth: 0,
|
|
4259
|
+
padding: inSheet ? 0 : "0 7px",
|
|
4260
|
+
border: "none",
|
|
4261
|
+
borderRadius: 6,
|
|
4262
|
+
cornerShape: "round",
|
|
4263
|
+
cursor: "pointer",
|
|
4264
|
+
fontSize: inSheet ? 14.5 : 11,
|
|
4265
|
+
lineHeight: (inSheet ? 32 : 22) + "px",
|
|
4266
|
+
fontFamily: "inherit",
|
|
4267
|
+
background: active
|
|
4268
|
+
? C.chip
|
|
4269
|
+
: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
4270
|
+
color: active ? C.accent : C.muted,
|
|
4271
|
+
fontWeight: active ? 600 : 400
|
|
4272
|
+
},
|
|
4273
|
+
children: "H" + lv
|
|
4274
|
+
}, "lv" + lv);
|
|
4275
|
+
})
|
|
4276
|
+
});
|
|
4277
|
+
};
|
|
4278
|
+
var levelsPopDockEl = (!sheetOpen && (levelsOpen || levelsClosing)) ? buildLevelsPop(false) : null;
|
|
4279
|
+
var levelsPopSheetEl = (sheetOpen && (levelsOpen || levelsClosing)) ? buildLevelsPop(true) : null;
|
|
4280
|
+
// dock toggle: the triangle tips toward the side it will move TO
|
|
4281
|
+
var dockBtnEl = iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 });
|
|
4282
|
+
// the curtain button: a surface with an arrow rising out of it
|
|
4283
|
+
var sheetBtnEl = react_jsx_runtime.jsx("button", {
|
|
4284
|
+
className: "dqt-sheet-btn",
|
|
4285
|
+
onClick: openSheet,
|
|
4286
|
+
title: T("sheet.open"),
|
|
4287
|
+
style: {
|
|
4288
|
+
width: 24,
|
|
4289
|
+
height: 24,
|
|
4290
|
+
padding: 0,
|
|
4291
|
+
display: "flex",
|
|
4292
|
+
alignItems: "center",
|
|
4293
|
+
justifyContent: "center",
|
|
4294
|
+
borderRadius: "50%",
|
|
4295
|
+
cornerShape: "round",
|
|
4296
|
+
border: "none",
|
|
4297
|
+
flex: "none",
|
|
4298
|
+
cursor: "pointer",
|
|
4299
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
4300
|
+
color: C.muted,
|
|
4301
|
+
transition: "background 0.15s ease, color 0.15s ease, opacity 0.2s ease"
|
|
4302
|
+
},
|
|
4303
|
+
onMouseEnter: function (e) {
|
|
4304
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
4305
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
4306
|
+
},
|
|
4307
|
+
onMouseLeave: function (e) {
|
|
4308
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
4309
|
+
e.currentTarget.style.color = C.muted;
|
|
4310
|
+
},
|
|
4311
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
4312
|
+
width: 14,
|
|
4313
|
+
height: 14,
|
|
4314
|
+
viewBox: "0 0 14 14",
|
|
4315
|
+
fill: "none",
|
|
4316
|
+
stroke: "currentColor",
|
|
4317
|
+
strokeWidth: 1.8,
|
|
4318
|
+
strokeLinecap: "round",
|
|
4319
|
+
strokeLinejoin: "round",
|
|
4320
|
+
style: { display: "block" },
|
|
4321
|
+
children: [
|
|
4322
|
+
react_jsx_runtime.jsx("rect", { x: 1.6, y: 3.2, width: 10.8, height: 9.2, rx: 1.6 }),
|
|
4323
|
+
react_jsx_runtime.jsx("path", { d: "M7 9.6V5.6M4.9 7.7L7 5.6l2.1 2.1" })
|
|
4324
|
+
]
|
|
4325
|
+
})
|
|
4326
|
+
});
|
|
4327
|
+
// collapse (docked panel only): thick SVG cross, nudged slightly down
|
|
4328
|
+
var headerCloseBtnEl = react_jsx_runtime.jsx("button", {
|
|
4329
|
+
onClick: function () { setOpen(false); },
|
|
4330
|
+
title: T("handle.collapse"),
|
|
4331
|
+
style: {
|
|
4332
|
+
width: 24,
|
|
4333
|
+
height: 24,
|
|
4334
|
+
display: "flex",
|
|
4335
|
+
alignItems: "center",
|
|
4336
|
+
justifyContent: "center",
|
|
4337
|
+
borderRadius: "50%",
|
|
4338
|
+
cornerShape: "round",
|
|
4339
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
4340
|
+
border: "none",
|
|
4341
|
+
color: C.muted,
|
|
4342
|
+
cursor: "pointer"
|
|
4343
|
+
},
|
|
4344
|
+
onMouseEnter: function (e) {
|
|
4345
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
4346
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
4347
|
+
},
|
|
4348
|
+
onMouseLeave: function (e) {
|
|
4349
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
4350
|
+
e.currentTarget.style.color = C.muted;
|
|
4351
|
+
},
|
|
4352
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
4353
|
+
width: 15,
|
|
4354
|
+
height: 15,
|
|
4355
|
+
viewBox: "0 0 24 24",
|
|
4356
|
+
style: { display: "block", transform: "translate(1px, 0px)", overflow: "visible" },
|
|
4357
|
+
fill: "none",
|
|
4358
|
+
stroke: "currentColor",
|
|
4359
|
+
strokeWidth: 3.4,
|
|
4360
|
+
strokeLinecap: "round",
|
|
4361
|
+
children: [
|
|
4362
|
+
// cross lines span 5..19, intersection (12,12) centered
|
|
4363
|
+
react_jsx_runtime.jsx("line", { x1: 5, y1: 5, x2: 19, y2: 19 }),
|
|
4364
|
+
react_jsx_runtime.jsx("line", { x1: 19, y1: 5, x2: 5, y2: 19 })
|
|
4365
|
+
]
|
|
4366
|
+
})
|
|
4367
|
+
});
|
|
4368
|
+
|
|
4369
|
+
// ---- docked header: grip bar on top + the header row (whole block draggable)
|
|
4370
|
+
var dockHeaderEl = react_jsx_runtime.jsx("div", {
|
|
4371
|
+
style: {
|
|
4372
|
+
flex: "none",
|
|
4373
|
+
cursor: "grab",
|
|
4374
|
+
userSelect: "none"
|
|
4375
|
+
},
|
|
4376
|
+
title: T("handle.dragY"),
|
|
4377
|
+
onPointerDown: onHandleDown,
|
|
4378
|
+
onMouseEnter: function () { handleBright(true); },
|
|
4379
|
+
onMouseLeave: function () { handleBright(false); },
|
|
4380
|
+
children: [
|
|
4381
|
+
react_jsx_runtime.jsx("div", {
|
|
4382
|
+
ref: handleRef,
|
|
4383
|
+
style: {
|
|
4384
|
+
width: 56,
|
|
4385
|
+
maxWidth: "calc(100% - 12px)",
|
|
4386
|
+
height: 5,
|
|
4387
|
+
borderRadius: 999,
|
|
4388
|
+
cornerShape: "round",
|
|
4389
|
+
background: "var(--dsw-alias-border-l2, rgba(128,128,128,0.55))",
|
|
4390
|
+
// no slack left in the header -> the grab hint fades away
|
|
4391
|
+
opacity: crowded ? 0 : 0.35,
|
|
4392
|
+
transition: "opacity 0.2s ease",
|
|
4393
|
+
margin: "5px auto 2px"
|
|
4394
|
+
}
|
|
4395
|
+
}),
|
|
4396
|
+
react_jsx_runtime.jsx("div", {
|
|
4397
|
+
ref: headerRef,
|
|
4398
|
+
className: "dqt-header",
|
|
4399
|
+
style: {
|
|
4400
|
+
display: "flex",
|
|
4401
|
+
alignItems: "center",
|
|
4402
|
+
justifyContent: "space-between",
|
|
4403
|
+
padding: "1px 10px 5px",
|
|
4404
|
+
borderBottom: "1px solid " + C.panelBorder,
|
|
4405
|
+
fontSize: 13,
|
|
4406
|
+
fontWeight: 600,
|
|
4407
|
+
gap: 6,
|
|
4408
|
+
// the controls keep their size and stay on ONE line; when the row
|
|
4409
|
+
// runs out of room it scrolls sideways (wheel) instead of wrapping or
|
|
4410
|
+
// overlapping, and the flexible spacer below collapses first
|
|
4411
|
+
flexWrap: "nowrap",
|
|
4412
|
+
overflowX: "auto",
|
|
4413
|
+
overflowY: "hidden",
|
|
4414
|
+
scrollbarWidth: "none"
|
|
4415
|
+
},
|
|
4416
|
+
children: [
|
|
4417
|
+
// LEFT pair: the heading-level filter and the dock toggle take the
|
|
4418
|
+
// left end — so the header reads two controls per side (search +
|
|
4419
|
+
// collapse sit at the other end).
|
|
3013
4420
|
react_jsx_runtime.jsx("div", {
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
width: 56,
|
|
3017
|
-
maxWidth: "calc(100% - 12px)",
|
|
3018
|
-
height: 5,
|
|
3019
|
-
borderRadius: 999,
|
|
3020
|
-
cornerShape: "round",
|
|
3021
|
-
background: "var(--dsw-alias-border-l2, rgba(128,128,128,0.55))",
|
|
3022
|
-
// no slack left in the header -> the grab hint fades away
|
|
3023
|
-
opacity: crowded ? 0 : 0.35,
|
|
3024
|
-
transition: "opacity 0.2s ease",
|
|
3025
|
-
margin: "5px auto 2px"
|
|
3026
|
-
}
|
|
4421
|
+
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
4422
|
+
children: [levelsBtnEl, questionsBtnEl, dockBtnEl]
|
|
3027
4423
|
}),
|
|
4424
|
+
// spacer: nothing here, it just pins the two pairs to the two ends —
|
|
4425
|
+
// and it is the first thing to give way when the panel narrows
|
|
4426
|
+
react_jsx_runtime.jsx("div", { style: { flex: "1 1 auto", minWidth: 0 } }),
|
|
4427
|
+
// RIGHT pair: search + curtain + collapse
|
|
3028
4428
|
react_jsx_runtime.jsx("div", {
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
style: {
|
|
3032
|
-
display: "flex",
|
|
3033
|
-
alignItems: "center",
|
|
3034
|
-
justifyContent: "space-between",
|
|
3035
|
-
padding: "1px 10px 5px",
|
|
3036
|
-
borderBottom: "1px solid " + C.panelBorder,
|
|
3037
|
-
fontSize: 13,
|
|
3038
|
-
fontWeight: 600,
|
|
3039
|
-
gap: 6,
|
|
3040
|
-
// the four controls keep their size and stay on ONE line; when the row
|
|
3041
|
-
// runs out of room it scrolls sideways (wheel) instead of wrapping or
|
|
3042
|
-
// overlapping, and the flexible spacer below collapses first
|
|
3043
|
-
flexWrap: "nowrap",
|
|
3044
|
-
overflowX: "auto",
|
|
3045
|
-
overflowY: "hidden",
|
|
3046
|
-
scrollbarWidth: "none"
|
|
3047
|
-
},
|
|
3048
|
-
children: [
|
|
3049
|
-
// LEFT pair: the heading-level filter and the dock toggle take the
|
|
3050
|
-
// left end — so the header reads two controls per side (search +
|
|
3051
|
-
// collapse sit at the other end).
|
|
3052
|
-
react_jsx_runtime.jsx("div", {
|
|
3053
|
-
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
3054
|
-
children: [
|
|
3055
|
-
// heading-level filter: a round icon button that pops down the
|
|
3056
|
-
// H1–H6 picker. Icon: three lines of decreasing width = outline
|
|
3057
|
-
// levels. Hover behaviour matches the magnifier: background +
|
|
3058
|
-
// icon tint change; the resting look never shifts when open.
|
|
3059
|
-
react_jsx_runtime.jsx("button", {
|
|
3060
|
-
className: "dqt-levels-btn",
|
|
3061
|
-
onClick: function () { levelsOpen ? closeLevels() : setLevelsOpen(true); },
|
|
3062
|
-
title: T("levels.tip"),
|
|
3063
|
-
style: {
|
|
3064
|
-
width: 24,
|
|
3065
|
-
height: 24,
|
|
3066
|
-
padding: 0,
|
|
3067
|
-
border: "none",
|
|
3068
|
-
borderRadius: "50%",
|
|
3069
|
-
cornerShape: "round",
|
|
3070
|
-
cursor: "pointer",
|
|
3071
|
-
flex: "none",
|
|
3072
|
-
display: "flex",
|
|
3073
|
-
alignItems: "center",
|
|
3074
|
-
justifyContent: "center",
|
|
3075
|
-
// open (or still animating shut) uses the SAME tint as every
|
|
3076
|
-
// other "on" control, so the header reads consistently
|
|
3077
|
-
background: (levelsOpen || levelsClosing) ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
3078
|
-
color: (levelsOpen || levelsClosing) ? C.accent : C.muted,
|
|
3079
|
-
transition: "background 0.15s ease, color 0.15s ease"
|
|
3080
|
-
},
|
|
3081
|
-
onMouseEnter: function (e) {
|
|
3082
|
-
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
3083
|
-
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
3084
|
-
},
|
|
3085
|
-
onMouseLeave: function (e) {
|
|
3086
|
-
e.currentTarget.style.background = (levelsOpen || levelsClosing) ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
3087
|
-
e.currentTarget.style.color = (levelsOpen || levelsClosing) ? C.accent : C.muted;
|
|
3088
|
-
},
|
|
3089
|
-
children: react_jsx_runtime.jsx("svg", {
|
|
3090
|
-
width: 14,
|
|
3091
|
-
height: 14,
|
|
3092
|
-
viewBox: "0 0 14 14",
|
|
3093
|
-
fill: "none",
|
|
3094
|
-
stroke: "currentColor",
|
|
3095
|
-
strokeWidth: 1.8,
|
|
3096
|
-
strokeLinecap: "round",
|
|
3097
|
-
style: { display: "block" },
|
|
3098
|
-
children: [
|
|
3099
|
-
react_jsx_runtime.jsx("line", { x1: 1.5, y1: 3, x2: 12.5, y2: 3 }),
|
|
3100
|
-
react_jsx_runtime.jsx("line", { x1: 1.5, y1: 7, x2: 9, y2: 7 }),
|
|
3101
|
-
react_jsx_runtime.jsx("line", { x1: 1.5, y1: 11, x2: 5.5, y2: 11 })
|
|
3102
|
-
]
|
|
3103
|
-
})
|
|
3104
|
-
}),
|
|
3105
|
-
// dock toggle: the triangle tips toward the side it will move TO
|
|
3106
|
-
iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 })
|
|
3107
|
-
]
|
|
3108
|
-
}),
|
|
3109
|
-
// spacer: nothing here, it just pins the two pairs to the two ends —
|
|
3110
|
-
// and it is the first thing to give way when the panel narrows
|
|
3111
|
-
react_jsx_runtime.jsx("div", { style: { flex: "1 1 auto", minWidth: 0 } }),
|
|
3112
|
-
// RIGHT pair: search + collapse
|
|
3113
|
-
react_jsx_runtime.jsx("div", {
|
|
3114
|
-
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
3115
|
-
children: [
|
|
3116
|
-
// magnifier button (SVG, matches the other buttons' style)
|
|
3117
|
-
react_jsx_runtime.jsx("button", {
|
|
3118
|
-
onClick: function () { searchOpen ? closeSearch() : openSearch(); },
|
|
3119
|
-
title: T("search.open"),
|
|
3120
|
-
style: {
|
|
3121
|
-
width: 24,
|
|
3122
|
-
height: 24,
|
|
3123
|
-
display: "flex",
|
|
3124
|
-
alignItems: "center",
|
|
3125
|
-
justifyContent: "center",
|
|
3126
|
-
borderRadius: "50%",
|
|
3127
|
-
cornerShape: "round",
|
|
3128
|
-
background: searchOpen ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
3129
|
-
border: "none",
|
|
3130
|
-
color: searchOpen ? C.accent : C.muted,
|
|
3131
|
-
cursor: "pointer"
|
|
3132
|
-
},
|
|
3133
|
-
onMouseEnter: function (e) {
|
|
3134
|
-
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
3135
|
-
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
3136
|
-
},
|
|
3137
|
-
onMouseLeave: function (e) {
|
|
3138
|
-
e.currentTarget.style.background = searchOpen ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
3139
|
-
e.currentTarget.style.color = searchOpen ? C.accent : C.muted;
|
|
3140
|
-
},
|
|
3141
|
-
children: react_jsx_runtime.jsx("svg", {
|
|
3142
|
-
width: 15,
|
|
3143
|
-
height: 15,
|
|
3144
|
-
viewBox: "0 0 24 24",
|
|
3145
|
-
// overflow visible: the stroke may extend past the
|
|
3146
|
-
// viewBox without being clipped at the svg boundary
|
|
3147
|
-
style: { display: "block", transform: "translate(-1px, -1px)", overflow: "visible" },
|
|
3148
|
-
fill: "none",
|
|
3149
|
-
stroke: "currentColor",
|
|
3150
|
-
strokeWidth: 3.6,
|
|
3151
|
-
strokeLinecap: "round",
|
|
3152
|
-
children: [
|
|
3153
|
-
react_jsx_runtime.jsx("circle", { cx: 11, cy: 11, r: 7 }),
|
|
3154
|
-
react_jsx_runtime.jsx("line", { x1: 21, y1: 21, x2: 16, y2: 16 })
|
|
3155
|
-
]
|
|
3156
|
-
})
|
|
3157
|
-
}),
|
|
3158
|
-
// close: thick SVG cross, nudged slightly down
|
|
3159
|
-
react_jsx_runtime.jsx("button", {
|
|
3160
|
-
onClick: function () { setOpen(false); },
|
|
3161
|
-
title: T("handle.collapse"),
|
|
3162
|
-
style: {
|
|
3163
|
-
width: 24,
|
|
3164
|
-
height: 24,
|
|
3165
|
-
display: "flex",
|
|
3166
|
-
alignItems: "center",
|
|
3167
|
-
justifyContent: "center",
|
|
3168
|
-
borderRadius: "50%",
|
|
3169
|
-
cornerShape: "round",
|
|
3170
|
-
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
3171
|
-
border: "none",
|
|
3172
|
-
color: C.muted,
|
|
3173
|
-
cursor: "pointer"
|
|
3174
|
-
},
|
|
3175
|
-
onMouseEnter: function (e) {
|
|
3176
|
-
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
3177
|
-
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
3178
|
-
},
|
|
3179
|
-
onMouseLeave: function (e) {
|
|
3180
|
-
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
3181
|
-
e.currentTarget.style.color = C.muted;
|
|
3182
|
-
},
|
|
3183
|
-
children: react_jsx_runtime.jsx("svg", {
|
|
3184
|
-
width: 15,
|
|
3185
|
-
height: 15,
|
|
3186
|
-
viewBox: "0 0 24 24",
|
|
3187
|
-
style: { display: "block", transform: "translate(1px, 0px)", overflow: "visible" },
|
|
3188
|
-
fill: "none",
|
|
3189
|
-
stroke: "currentColor",
|
|
3190
|
-
strokeWidth: 3.4,
|
|
3191
|
-
strokeLinecap: "round",
|
|
3192
|
-
children: [
|
|
3193
|
-
// cross lines span 5..19, intersection (12,12) centered
|
|
3194
|
-
react_jsx_runtime.jsx("line", { x1: 5, y1: 5, x2: 19, y2: 19 }),
|
|
3195
|
-
react_jsx_runtime.jsx("line", { x1: 19, y1: 5, x2: 5, y2: 19 })
|
|
3196
|
-
]
|
|
3197
|
-
})
|
|
3198
|
-
})
|
|
3199
|
-
]
|
|
3200
|
-
})
|
|
3201
|
-
]
|
|
4429
|
+
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
4430
|
+
children: [searchBtnEl, sheetBtnEl, headerCloseBtnEl]
|
|
3202
4431
|
})
|
|
3203
4432
|
]
|
|
4433
|
+
})
|
|
4434
|
+
]
|
|
4435
|
+
});
|
|
4436
|
+
|
|
4437
|
+
// ---- curtain chrome: the controls that still apply here (the level filter,
|
|
4438
|
+
// questions-only, search) plus the way out. There is no title — the surface is
|
|
4439
|
+
// self-evident — and no hint text: ✕ and Esc are the obvious exits. The dock /
|
|
4440
|
+
// curtain / collapse buttons are absent on purpose: there is no docked panel to
|
|
4441
|
+
// move and no second curtain to open.
|
|
4442
|
+
var sheetChromeEl = react_jsx_runtime.jsx("div", {
|
|
4443
|
+
className: "dqt-sheet-chrome",
|
|
4444
|
+
title: T("sheet.noDrag"),
|
|
4445
|
+
style: {
|
|
4446
|
+
flex: "none",
|
|
4447
|
+
display: "flex",
|
|
4448
|
+
alignItems: "center",
|
|
4449
|
+
gap: 10,
|
|
4450
|
+
padding: "7px 12px",
|
|
4451
|
+
borderBottom: "1px solid " + C.panelBorder
|
|
4452
|
+
},
|
|
4453
|
+
children: [
|
|
4454
|
+
react_jsx_runtime.jsx("div", {
|
|
4455
|
+
style: { position: "relative", display: "flex", alignItems: "center", gap: 8, flex: "none" },
|
|
4456
|
+
children: [levelsBtnEl, questionsBtnEl, levelsPopSheetEl]
|
|
3204
4457
|
}),
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
4458
|
+
react_jsx_runtime.jsx("div", { style: { flex: "1 1 auto", minWidth: 0 } }),
|
|
4459
|
+
searchBtnEl,
|
|
4460
|
+
sheetCloseBtnEl
|
|
4461
|
+
]
|
|
4462
|
+
});
|
|
4463
|
+
|
|
4464
|
+
// ---- search row: the input plus the scope pill, the fuzzy switch and the
|
|
4465
|
+
// match counter. In the docked layout it is a row under the header; on the
|
|
4466
|
+
// curtain it is the head of the search column on the right.
|
|
4467
|
+
var searchRowEl = (searchOpen || searchAnim === "out") ? react_jsx_runtime.jsx("div", {
|
|
4468
|
+
style: {
|
|
4469
|
+
display: "grid",
|
|
4470
|
+
gridTemplateRows: (searchAnim === "enter" || searchAnim === "out") ? "0fr" : "1fr",
|
|
4471
|
+
transition: "grid-template-rows 0.2s " + EASE,
|
|
4472
|
+
flex: "none"
|
|
4473
|
+
},
|
|
4474
|
+
children: react_jsx_runtime.jsx("div", {
|
|
4475
|
+
style: { overflow: "hidden", minHeight: 0 },
|
|
4476
|
+
children: react_jsx_runtime.jsx("div", {
|
|
3209
4477
|
style: {
|
|
3210
|
-
display: "
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
4478
|
+
display: "flex",
|
|
4479
|
+
alignItems: "center",
|
|
4480
|
+
gap: sheetOpen ? 6 : 4,
|
|
4481
|
+
padding: sheetOpen ? "4px 10px 8px" : "2px 8px 4px",
|
|
4482
|
+
borderBottom: "1px solid " + C.panelBorder,
|
|
4483
|
+
opacity: (searchAnim === "enter" || searchAnim === "out") ? 0 : 1,
|
|
4484
|
+
transform: (searchAnim === "enter" || searchAnim === "out") ? "translateY(-6px)" : "none",
|
|
4485
|
+
transition: "opacity 0.2s ease, transform 0.2s ease"
|
|
3214
4486
|
},
|
|
3215
|
-
children:
|
|
3216
|
-
style: { overflow: "hidden", minHeight: 0 },
|
|
3217
|
-
children: react_jsx_runtime.jsx("div", {
|
|
3218
|
-
style: {
|
|
3219
|
-
display: "flex",
|
|
3220
|
-
alignItems: "center",
|
|
3221
|
-
gap: 4,
|
|
3222
|
-
padding: "2px 8px 4px",
|
|
3223
|
-
borderBottom: "1px solid " + C.panelBorder,
|
|
3224
|
-
opacity: (searchAnim === "enter" || searchAnim === "out") ? 0 : 1,
|
|
3225
|
-
transform: (searchAnim === "enter" || searchAnim === "out") ? "translateY(-6px)" : "none",
|
|
3226
|
-
transition: "opacity 0.2s ease, transform 0.2s ease"
|
|
3227
|
-
},
|
|
3228
|
-
children: [
|
|
4487
|
+
children: [
|
|
3229
4488
|
// input with a custom animated placeholder: the box stays static,
|
|
3230
4489
|
// only the placeholder text fades when the scope switches
|
|
3231
4490
|
react_jsx_runtime.jsx("div", {
|
|
@@ -3238,7 +4497,7 @@ window.__ModuleLoader__.load({
|
|
|
3238
4497
|
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
3239
4498
|
borderRadius: 999,
|
|
3240
4499
|
cornerShape: "round",
|
|
3241
|
-
padding: "6px 12px"
|
|
4500
|
+
padding: sheetOpen ? "8px 14px" : "6px 12px"
|
|
3242
4501
|
},
|
|
3243
4502
|
children: [
|
|
3244
4503
|
react_jsx_runtime.jsx("input", {
|
|
@@ -3246,8 +4505,12 @@ window.__ModuleLoader__.load({
|
|
|
3246
4505
|
value: query,
|
|
3247
4506
|
onChange: function (e) { onQueryChange(e.target.value); },
|
|
3248
4507
|
onKeyDown: function (e) {
|
|
3249
|
-
|
|
3250
|
-
|
|
4508
|
+
// ↑/↓ step the in-session matches, exactly like Enter does
|
|
4509
|
+
// (arrows only mean something while a query is typed)
|
|
4510
|
+
if (e.key === "ArrowDown") { goToMatch(matchIdx + 1); swallow(e); return; }
|
|
4511
|
+
if (e.key === "ArrowUp") { goToMatch(matchIdx - 1); swallow(e); return; }
|
|
4512
|
+
if (e.key === "Enter") { goToMatch(matchIdx + 1); swallow(e); return; }
|
|
4513
|
+
if (e.key === "Escape") { closeSearch(); swallow(e); }
|
|
3251
4514
|
},
|
|
3252
4515
|
style: {
|
|
3253
4516
|
flex: 1,
|
|
@@ -3256,7 +4519,7 @@ window.__ModuleLoader__.load({
|
|
|
3256
4519
|
background: "transparent",
|
|
3257
4520
|
border: "none",
|
|
3258
4521
|
outline: "none",
|
|
3259
|
-
fontSize: 13,
|
|
4522
|
+
fontSize: sheetOpen ? 16 : 13,
|
|
3260
4523
|
color: C.text,
|
|
3261
4524
|
padding: 0
|
|
3262
4525
|
}
|
|
@@ -3264,9 +4527,9 @@ window.__ModuleLoader__.load({
|
|
|
3264
4527
|
(!query) ? react_jsx_runtime.jsx("span", {
|
|
3265
4528
|
style: {
|
|
3266
4529
|
position: "absolute",
|
|
3267
|
-
left: 12,
|
|
4530
|
+
left: sheetOpen ? 14 : 12,
|
|
3268
4531
|
pointerEvents: "none",
|
|
3269
|
-
fontSize: 13,
|
|
4532
|
+
fontSize: sheetOpen ? 16 : 13,
|
|
3270
4533
|
color: "var(--dsw-alias-label-tertiary, rgba(128,128,128,0.7))"
|
|
3271
4534
|
},
|
|
3272
4535
|
children: [
|
|
@@ -3277,21 +4540,21 @@ window.__ModuleLoader__.load({
|
|
|
3277
4540
|
react_jsx_runtime.jsx("span", {
|
|
3278
4541
|
key: searchScope,
|
|
3279
4542
|
style: { display: "inline-block", animation: "dqt-fade-in 0.25s linear" },
|
|
3280
|
-
children: searchScope
|
|
4543
|
+
children: scopeLabel(searchScope)
|
|
3281
4544
|
}),
|
|
3282
4545
|
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
3283
4546
|
key: "old-" + prevScope,
|
|
3284
4547
|
style: { position: "absolute", left: 0, top: 0, opacity: 0, animation: "dqt-fade-out 0.25s linear forwards" },
|
|
3285
|
-
children: prevScope
|
|
4548
|
+
children: scopeLabel(prevScope)
|
|
3286
4549
|
}) : null
|
|
3287
4550
|
]
|
|
3288
4551
|
}),
|
|
3289
4552
|
T("search.tail")
|
|
3290
4553
|
]
|
|
3291
|
-
}) : null
|
|
4554
|
+
}) : null
|
|
3292
4555
|
]
|
|
3293
4556
|
}),
|
|
3294
|
-
searching ? react_jsx_runtime.jsx("span", {
|
|
4557
|
+
(searching || (searchScope === "cross" && cross.phase === "loading")) ? react_jsx_runtime.jsx("span", {
|
|
3295
4558
|
style: { width: 13, height: 13, flex: "none", display: "flex", alignItems: "center", justifyContent: "center" },
|
|
3296
4559
|
children: react_jsx_runtime.jsx("svg", {
|
|
3297
4560
|
width: 12,
|
|
@@ -3309,25 +4572,30 @@ window.__ModuleLoader__.load({
|
|
|
3309
4572
|
})
|
|
3310
4573
|
}) : react_jsx_runtime.jsx("span", {
|
|
3311
4574
|
style: { fontSize: 11, color: C.muted, flex: "none", minWidth: 30, textAlign: "center" },
|
|
3312
|
-
children:
|
|
4575
|
+
children: searchScope === "cross"
|
|
4576
|
+
? (cross.phase === "done" && cross.rows.length > 0 ? String(cross.rows.length) + (cross.more ? "+" : "") : "")
|
|
4577
|
+
: (matches.length > 0 ? ((matchIdx % matches.length) + 1) + "/" + matches.length : "")
|
|
3313
4578
|
}),
|
|
3314
|
-
// scope toggle: 标题
|
|
3315
|
-
// label text cross-fades old->new
|
|
4579
|
+
// scope toggle: 标题 -> 全文 -> 会话 (ONE round pill cycling the three
|
|
4580
|
+
// scopes; background fades, label text cross-fades old->new, so a third
|
|
4581
|
+
// scope costs no extra button)
|
|
3316
4582
|
react_jsx_runtime.jsx("button", {
|
|
3317
4583
|
onClick: toggleScope,
|
|
3318
|
-
|
|
4584
|
+
"data-scope": searchScope,
|
|
4585
|
+
title: searchScope === "title" ? T("search.scope.tipTitle")
|
|
4586
|
+
: (searchScope === "full" ? T("search.scope.tipFull") : T("search.scope.tipCross")),
|
|
3319
4587
|
style: {
|
|
3320
4588
|
flex: "none",
|
|
3321
|
-
width: 34,
|
|
3322
|
-
height: 34,
|
|
4589
|
+
width: sheetOpen ? 40 : 34,
|
|
4590
|
+
height: sheetOpen ? 40 : 34,
|
|
3323
4591
|
display: "flex",
|
|
3324
4592
|
alignItems: "center",
|
|
3325
4593
|
justifyContent: "center",
|
|
3326
4594
|
borderRadius: "50%",
|
|
3327
4595
|
cornerShape: "round",
|
|
3328
|
-
fontSize: 11,
|
|
3329
|
-
color: searchScope
|
|
3330
|
-
background: searchScope
|
|
4596
|
+
fontSize: sheetOpen ? 13 : 11,
|
|
4597
|
+
color: searchScope !== "title" ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
4598
|
+
background: searchScope !== "title" ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
3331
4599
|
border: "none",
|
|
3332
4600
|
cursor: "pointer",
|
|
3333
4601
|
whiteSpace: "nowrap",
|
|
@@ -3339,91 +4607,272 @@ window.__ModuleLoader__.load({
|
|
|
3339
4607
|
react_jsx_runtime.jsx("span", {
|
|
3340
4608
|
key: searchScope,
|
|
3341
4609
|
style: { animation: "dqt-fade-in 0.25s linear", display: "block" },
|
|
3342
|
-
children: searchScope
|
|
4610
|
+
children: scopeLabel(searchScope)
|
|
3343
4611
|
}),
|
|
3344
4612
|
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
3345
4613
|
key: "old-" + prevScope,
|
|
3346
4614
|
style: { position: "absolute", opacity: 0, animation: "dqt-fade-out 0.25s linear forwards", display: "block" },
|
|
3347
|
-
children: prevScope
|
|
4615
|
+
children: scopeLabel(prevScope)
|
|
3348
4616
|
}) : null
|
|
3349
4617
|
]
|
|
3350
4618
|
})
|
|
3351
4619
|
}),
|
|
3352
4620
|
// fuzzy switch: an independent toggle beside the scope pill (its label
|
|
3353
|
-
// is the same either way, so only the colour carries the state)
|
|
4621
|
+
// is the same either way, so only the colour carries the state). It means
|
|
4622
|
+
// nothing in cross-session scope — the HOST searches literal phrases —
|
|
4623
|
+
// so there it is disabled and its tip says exactly that.
|
|
3354
4624
|
react_jsx_runtime.jsx("button", {
|
|
3355
4625
|
onClick: toggleFuzzy,
|
|
3356
|
-
|
|
4626
|
+
disabled: searchScope === "cross",
|
|
4627
|
+
title: searchScope === "cross" ? T("search.cross.note") : (fuzzy ? T("search.fuzzy.tipOn") : T("search.fuzzy.tipOff")),
|
|
3357
4628
|
"data-fuzzy": fuzzy ? "on" : "off",
|
|
3358
4629
|
style: {
|
|
3359
4630
|
flex: "none",
|
|
3360
|
-
width: 34,
|
|
3361
|
-
height: 34,
|
|
4631
|
+
width: sheetOpen ? 40 : 34,
|
|
4632
|
+
height: sheetOpen ? 40 : 34,
|
|
3362
4633
|
display: "flex",
|
|
3363
4634
|
alignItems: "center",
|
|
3364
4635
|
justifyContent: "center",
|
|
3365
4636
|
borderRadius: "50%",
|
|
3366
4637
|
cornerShape: "round",
|
|
3367
|
-
fontSize: 11,
|
|
4638
|
+
fontSize: sheetOpen ? 13 : 11,
|
|
3368
4639
|
color: fuzzy ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
3369
4640
|
background: fuzzy ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
3370
4641
|
border: "none",
|
|
3371
|
-
cursor: "pointer",
|
|
4642
|
+
cursor: searchScope === "cross" ? "default" : "pointer",
|
|
4643
|
+
opacity: searchScope === "cross" ? 0.45 : 1,
|
|
3372
4644
|
whiteSpace: "nowrap",
|
|
3373
|
-
transition: "background 0.25s ease, color 0.25s ease"
|
|
4645
|
+
transition: "background 0.25s ease, color 0.25s ease, opacity 0.25s ease"
|
|
3374
4646
|
},
|
|
3375
4647
|
children: T("search.fuzzy")
|
|
3376
4648
|
})
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
4649
|
+
]
|
|
4650
|
+
})
|
|
4651
|
+
})
|
|
4652
|
+
}) : null;
|
|
4653
|
+
|
|
4654
|
+
// ---- list content: the outline, or the search results ---------------------
|
|
4655
|
+
// Built once and placed either inside the single docked list, or split across
|
|
4656
|
+
// the curtain's two columns (outline left, results right).
|
|
4657
|
+
var resultsEl = query.trim()
|
|
4658
|
+
? (searchScope === "cross"
|
|
4659
|
+
? react_jsx_runtime.jsx("div", {
|
|
4660
|
+
key: "cross",
|
|
4661
|
+
children: renderCrossResults(cross, query.trim(), C, crossTitle, openCrossHit)
|
|
4662
|
+
})
|
|
4663
|
+
: react_jsx_runtime.jsx("div", {
|
|
4664
|
+
key: "results",
|
|
4665
|
+
children: renderResults(resultRows, query.trim(), C, goToRow, activeResultRow, renderExtra)
|
|
4666
|
+
}))
|
|
4667
|
+
: null;
|
|
4668
|
+
// older history is one upward scroll away — say so until the reader actually
|
|
4669
|
+
// scrolls up (then the hint has done its job)
|
|
4670
|
+
var hintEl = hint ? react_jsx_runtime.jsx("div", {
|
|
4671
|
+
key: "hint",
|
|
4672
|
+
style: {
|
|
4673
|
+
display: "flex",
|
|
4674
|
+
alignItems: "center",
|
|
4675
|
+
justifyContent: "center",
|
|
4676
|
+
padding: "10px 6px 4px",
|
|
4677
|
+
fontSize: 11,
|
|
4678
|
+
color: C.muted,
|
|
4679
|
+
opacity: 0.85,
|
|
4680
|
+
textAlign: "center"
|
|
4681
|
+
},
|
|
4682
|
+
children: hint === "oldest" ? T("search.hintOldest") : T("search.hintMore")
|
|
4683
|
+
}) : null;
|
|
4684
|
+
var outlineEl = renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup,
|
|
4685
|
+
questionsOnly ? Object.assign({}, renderExtra, { questionsOnly: true }) : renderExtra);
|
|
4686
|
+
// The curtain keeps the outline in the middle of the surface (its own column,
|
|
4687
|
+
// centred with room to spare at both sides) and pushes the search in from the
|
|
4688
|
+
// right; the docked panel keeps its original "results replace the list".
|
|
4689
|
+
var listChildren = sheetOpen
|
|
4690
|
+
? [outlineEl, hintEl]
|
|
4691
|
+
: (resultsEl ? [resultsEl, hintEl] : outlineEl);
|
|
4692
|
+
var listEl = react_jsx_runtime.jsx("div", {
|
|
4693
|
+
ref: listRef,
|
|
4694
|
+
className: "dqt-list",
|
|
4695
|
+
// focusable so the arrow keys can drive the cursor; the panel root handles
|
|
4696
|
+
// the same keys, so either focus target works
|
|
4697
|
+
tabIndex: 0,
|
|
4698
|
+
role: "group",
|
|
4699
|
+
"aria-label": T("a11y.list"),
|
|
4700
|
+
style: {
|
|
4701
|
+
overflowY: "auto",
|
|
4702
|
+
// NO top padding: a sticky child is confined to its containing
|
|
4703
|
+
// block, so a padded scroll container would hold the pinned group
|
|
4704
|
+
// header that many pixels below the toolbar (a visible gap). Spacing
|
|
4705
|
+
// between groups comes from the per-group divider instead.
|
|
4706
|
+
padding: sheetOpen ? "0 20px 8px" : "0 8px 6px",
|
|
4707
|
+
flex: "1 1 auto",
|
|
4708
|
+
minHeight: 0,
|
|
4709
|
+
// across the curtain the list does not touch the edges: it is a centred
|
|
4710
|
+
// column with room at both sides, which is also what makes it slide left
|
|
4711
|
+
// when the search column is pushed in
|
|
4712
|
+
width: sheetOpen ? "100%" : undefined,
|
|
4713
|
+
maxWidth: sheetOpen ? "90%" : undefined,
|
|
4714
|
+
margin: sheetOpen ? "0 auto" : undefined,
|
|
4715
|
+
// the 20px side padding sits INSIDE the 90% (content-box would make the
|
|
4716
|
+
// rendered column 90% + 40px, i.e. ~93% of the curtain)
|
|
4717
|
+
boxSizing: sheetOpen ? "border-box" : undefined,
|
|
4718
|
+
// the scrollbar follows the dock side in the narrow panel; across the
|
|
4719
|
+
// curtain it sits on the list's right edge
|
|
4720
|
+
direction: (sheetOpen || dockRight) ? "ltr" : "rtl"
|
|
4721
|
+
},
|
|
4722
|
+
onScroll: onListScroll,
|
|
4723
|
+
onWheel: onListWheel,
|
|
4724
|
+
children: react_jsx_runtime.jsx("div", {
|
|
4725
|
+
// the two-phase cross-fade of the questions-only switch rides on this
|
|
4726
|
+
// wrapper (classes, never inline animation: React rewrites inline styles
|
|
4727
|
+
// every render and would restart or drop the animation)
|
|
4728
|
+
className: "dqt-list-body"
|
|
4729
|
+
+ (listFade === "out" ? " dqt-mode-out" : (listFade === "in" ? " dqt-mode-in" : "")),
|
|
4730
|
+
style: { direction: "ltr" },
|
|
4731
|
+
children: listChildren
|
|
4732
|
+
})
|
|
4733
|
+
});
|
|
4734
|
+
// ---- curtain search column: pushed in from the right edge -----------------
|
|
4735
|
+
// The panel's own search box and its two buttons, then the hits below them.
|
|
4736
|
+
// Only the column's WIDTH animates (0 <-> 360) so the outline is pushed left
|
|
4737
|
+
// instead of jumped; an inner box keeps the full width so the content never
|
|
4738
|
+
// squashes while the column narrows, and the column is always mounted while the
|
|
4739
|
+
// curtain is up so both directions animate.
|
|
4740
|
+
var sheetColW = (searchOpen || searchAnim === "out") ? SHEET_SEARCH_W : 0;
|
|
4741
|
+
var sheetSearchColEl = sheetOpen ? react_jsx_runtime.jsx("div", {
|
|
4742
|
+
className: "dqt-sheet-search",
|
|
4743
|
+
"data-sheet-search": sheetColW > 0 ? "on" : undefined,
|
|
4744
|
+
style: {
|
|
4745
|
+
flex: "none",
|
|
4746
|
+
width: sheetColW,
|
|
4747
|
+
overflow: "hidden",
|
|
4748
|
+
minHeight: 0,
|
|
4749
|
+
borderLeft: sheetColW > 0 ? "1px solid " + C.panelBorder : "none",
|
|
4750
|
+
transition: "width 0.24s " + EASE + ", border-color 0.24s " + EASE
|
|
4751
|
+
},
|
|
4752
|
+
children: react_jsx_runtime.jsx("div", {
|
|
4753
|
+
style: { width: SHEET_SEARCH_W, height: "100%", display: "flex", flexDirection: "column", minHeight: 0 },
|
|
4754
|
+
children: [
|
|
4755
|
+
searchRowEl,
|
|
4756
|
+
// empty until something is typed — no placeholder note in the way
|
|
4757
|
+
react_jsx_runtime.jsx("div", {
|
|
4758
|
+
className: "dqt-sheet-results",
|
|
4759
|
+
style: { flex: "1 1 auto", minHeight: 0, overflowY: "auto", padding: "0 8px 8px", direction: "ltr" },
|
|
4760
|
+
children: resultsEl
|
|
3425
4761
|
})
|
|
3426
|
-
|
|
4762
|
+
]
|
|
4763
|
+
})
|
|
4764
|
+
}) : null;
|
|
4765
|
+
// the outline and the search column share one row in BOTH layouts, so the list
|
|
4766
|
+
// is never moved between parents when the curtain opens or closes
|
|
4767
|
+
var bodyRowEl = react_jsx_runtime.jsx("div", {
|
|
4768
|
+
className: "dqt-body-row",
|
|
4769
|
+
style: { flex: "1 1 auto", minHeight: 0, display: "flex", flexDirection: "row", alignItems: "stretch" },
|
|
4770
|
+
children: [listEl, sheetSearchColEl]
|
|
4771
|
+
});
|
|
4772
|
+
var panelEl = react_jsx_runtime.jsx("div", {
|
|
4773
|
+
ref: panelRootRef,
|
|
4774
|
+
// the panel takes focus on any click inside it (except into a text field), so
|
|
4775
|
+
// ↑/↓/Enter/Esc keep working right after a mouse interaction instead of only
|
|
4776
|
+
// when the user tabs into the widget first
|
|
4777
|
+
tabIndex: -1,
|
|
4778
|
+
onKeyDown: onPanelKeyDown,
|
|
4779
|
+
onMouseDown: function (e) {
|
|
4780
|
+
var t = e.target;
|
|
4781
|
+
var tag = t && t.tagName ? String(t.tagName).toUpperCase() : "";
|
|
4782
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || (t && t.isContentEditable)) return;
|
|
4783
|
+
var root = panelRootRef.current;
|
|
4784
|
+
if (root && typeof root.focus === "function") root.focus();
|
|
4785
|
+
},
|
|
4786
|
+
style: sheetOpen ? {
|
|
4787
|
+
// inside the sheet the panel IS the content: the wrapper owns position,
|
|
4788
|
+
// size and the drop-in animation, and the content renders at 100% (the
|
|
4789
|
+
// panel-scale preference is a docked-panel setting, not a sheet one)
|
|
4790
|
+
position: "relative",
|
|
4791
|
+
width: "100%",
|
|
4792
|
+
flex: "1 1 auto",
|
|
4793
|
+
minHeight: 0,
|
|
4794
|
+
display: "flex",
|
|
4795
|
+
flexDirection: "column",
|
|
4796
|
+
background: C.panelBg,
|
|
4797
|
+
border: "none",
|
|
4798
|
+
borderRadius: 0,
|
|
4799
|
+
zIndex: Z_PANEL,
|
|
4800
|
+
overflow: "hidden",
|
|
4801
|
+
color: C.text,
|
|
4802
|
+
opacity: 1,
|
|
4803
|
+
pointerEvents: chatViewActive ? "auto" : "none"
|
|
4804
|
+
} : {
|
|
4805
|
+
position: "fixed",
|
|
4806
|
+
top: zLen(baseTopPx),
|
|
4807
|
+
left: zLen(panelLeft),
|
|
4808
|
+
width: zLen(panelW),
|
|
4809
|
+
maxHeight: zLen(vMaxH),
|
|
4810
|
+
height: panelH > 0 ? zLen(panelH) : undefined,
|
|
4811
|
+
zoom: zoom === 1 ? undefined : zoom,
|
|
4812
|
+
display: "flex",
|
|
4813
|
+
flexDirection: "column",
|
|
4814
|
+
background: C.panelBg,
|
|
4815
|
+
border: "none",
|
|
4816
|
+
borderRadius: 12,
|
|
4817
|
+
zIndex: Z_PANEL,
|
|
4818
|
+
overflow: "hidden",
|
|
4819
|
+
color: C.text,
|
|
4820
|
+
opacity: panelOpacity,
|
|
4821
|
+
clipPath: panelClip,
|
|
4822
|
+
// inner shadow only (no outer): theme-aware (white bevel in dark mode,
|
|
4823
|
+
// soft shade in light) — outer shadow would be clipped by the collapse
|
|
4824
|
+
// clip-path and the panel edges.
|
|
4825
|
+
boxShadow: innerShadow(),
|
|
4826
|
+
// A view switch swaps in a short, delay-free opacity fade; the dock and
|
|
4827
|
+
// collapse animations keep their own (slower) timings.
|
|
4828
|
+
transition: viewFading ? "opacity 0.3s ease" : panelTransition,
|
|
4829
|
+
// held back while the edge handle slips home (see edgeLeaving): the panel is
|
|
4830
|
+
// already at its open geometry here, only the animation waits
|
|
4831
|
+
transitionDelay: edgeLeaving ? EDGE_OUT_MS + "ms" : undefined,
|
|
4832
|
+
pointerEvents: chatViewActive ? "auto" : "none"
|
|
4833
|
+
},
|
|
4834
|
+
onMouseEnter: function () { setHovered(true); },
|
|
4835
|
+
onMouseLeave: function () { setHovered(false); },
|
|
4836
|
+
onClickCapture: function (e) {
|
|
4837
|
+
// jump handled in the capture phase (bubble-phase handlers are unreliable here)
|
|
4838
|
+
var t = e.target;
|
|
4839
|
+
// the "to the section's end" control sits INSIDE a jumpable row, and the
|
|
4840
|
+
// row's own jump would otherwise fire first (capture runs top-down), so
|
|
4841
|
+
// this branch is checked first and takes the event entirely
|
|
4842
|
+
var endBtn = (t && t.closest) ? t.closest("[data-dqt-end]") : null;
|
|
4843
|
+
if (endBtn && endBtn.dataset) {
|
|
4844
|
+
e.preventDefault();
|
|
4845
|
+
e.stopPropagation();
|
|
4846
|
+
if (endBtn.dataset.endTurn !== undefined) {
|
|
4847
|
+
endJumpTurn(endBtn.dataset.endKey);
|
|
4848
|
+
} else {
|
|
4849
|
+
endJumpRow(endBtn.dataset.endKey, endBtn.dataset.endIdx !== undefined ? Number(endBtn.dataset.endIdx) : 0);
|
|
4850
|
+
}
|
|
4851
|
+
// same hand-back rule as picking a row: the curtain closes
|
|
4852
|
+
if (sheetOpen) closeSheet();
|
|
4853
|
+
return;
|
|
4854
|
+
}
|
|
4855
|
+
var item = (t && t.closest) ? t.closest("[data-jump-key]") : null;
|
|
4856
|
+
if (item && item.dataset && item.dataset.jumpKey) {
|
|
4857
|
+
e.preventDefault();
|
|
4858
|
+
e.stopPropagation();
|
|
4859
|
+
var idx = item.dataset.jumpIdx !== undefined ? Number(item.dataset.jumpIdx) : 0;
|
|
4860
|
+
jump(item.dataset.jumpKey, idx);
|
|
4861
|
+
// a curtain hands you back to the conversation once you picked a row
|
|
4862
|
+
if (sheetOpen) closeSheet();
|
|
4863
|
+
return;
|
|
4864
|
+
}
|
|
4865
|
+
// ANY other row (group header, in-session result, cross-session hit)
|
|
4866
|
+
// carries its own click handler and keeps it — no stopPropagation here —
|
|
4867
|
+
// but picking a row still hands the reader back to the conversation
|
|
4868
|
+
if (sheetOpen && t && t.closest && t.closest("[data-nav-row]")) closeSheet();
|
|
4869
|
+
},
|
|
4870
|
+
children: [
|
|
4871
|
+
sheetOpen ? sheetChromeEl : dockHeaderEl,
|
|
4872
|
+
// the docked panel keeps its search band under the header; across the
|
|
4873
|
+
// curtain the same row lives at the top of the pushed-in search column
|
|
4874
|
+
sheetOpen ? null : searchRowEl,
|
|
4875
|
+
bodyRowEl,
|
|
3427
4876
|
// "back to the newest row": floats over the list's lower-right corner and
|
|
3428
4877
|
// fades in the moment the newest turns fall below the fold. Kept MOUNTED
|
|
3429
4878
|
// and toggled by opacity/pointer-events — an unmounted button has nothing
|
|
@@ -3446,8 +4895,9 @@ window.__ModuleLoader__.load({
|
|
|
3446
4895
|
},
|
|
3447
4896
|
style: {
|
|
3448
4897
|
position: "absolute",
|
|
3449
|
-
// clear of the resize handle in the very corner (16x16)
|
|
3450
|
-
|
|
4898
|
+
// clear of the resize handle in the very corner (16x16); across the
|
|
4899
|
+
// curtain it also clears the pushed-in search column
|
|
4900
|
+
right: (sheetOpen && sheetColW > 0 ? SHEET_SEARCH_W : 0) + 18,
|
|
3451
4901
|
bottom: 12,
|
|
3452
4902
|
width: 26,
|
|
3453
4903
|
height: 26,
|
|
@@ -3490,10 +4940,19 @@ window.__ModuleLoader__.load({
|
|
|
3490
4940
|
})
|
|
3491
4941
|
}),
|
|
3492
4942
|
toastEl,
|
|
4943
|
+
// Polite live region: match stepping, result counts and cursor moves land
|
|
4944
|
+
// here. Visually hidden (`.dqt-sr`) — it exists for assistive tech only.
|
|
4945
|
+
react_jsx_runtime.jsx("span", {
|
|
4946
|
+
className: "dqt-sr",
|
|
4947
|
+
role: "status",
|
|
4948
|
+
"aria-live": "polite",
|
|
4949
|
+
"aria-atomic": "true",
|
|
4950
|
+
children: liveText
|
|
4951
|
+
}),
|
|
3493
4952
|
// resize handles (right edge: width, bottom edge: height)
|
|
3494
4953
|
// Their grab strips keep their SCREEN thickness under the scale factor:
|
|
3495
4954
|
// they are part of the panel's box, not of its magnified content.
|
|
3496
|
-
react_jsx_runtime.jsx("div", {
|
|
4955
|
+
sheetOpen ? null : react_jsx_runtime.jsx("div", {
|
|
3497
4956
|
style: {
|
|
3498
4957
|
position: "absolute",
|
|
3499
4958
|
top: 0,
|
|
@@ -3507,7 +4966,7 @@ window.__ModuleLoader__.load({
|
|
|
3507
4966
|
onPointerDown: onResizeWDown,
|
|
3508
4967
|
title: T("resize.w")
|
|
3509
4968
|
}),
|
|
3510
|
-
react_jsx_runtime.jsx("div", {
|
|
4969
|
+
sheetOpen ? null : react_jsx_runtime.jsx("div", {
|
|
3511
4970
|
style: {
|
|
3512
4971
|
position: "absolute",
|
|
3513
4972
|
left: 0,
|
|
@@ -3521,8 +4980,8 @@ window.__ModuleLoader__.load({
|
|
|
3521
4980
|
onPointerDown: onResizeHDown,
|
|
3522
4981
|
title: T("resize.h")
|
|
3523
4982
|
}),
|
|
3524
|
-
// corner handle: resize width AND height at once
|
|
3525
|
-
react_jsx_runtime.jsx("div", {
|
|
4983
|
+
// corner handle: resize width AND height at once (dock-only too)
|
|
4984
|
+
sheetOpen ? null : react_jsx_runtime.jsx("div", {
|
|
3526
4985
|
style: {
|
|
3527
4986
|
position: "absolute",
|
|
3528
4987
|
right: 0,
|
|
@@ -3536,88 +4995,54 @@ window.__ModuleLoader__.load({
|
|
|
3536
4995
|
onPointerDown: onResizeCornerDown,
|
|
3537
4996
|
title: T("resize.wh")
|
|
3538
4997
|
}),
|
|
3539
|
-
|
|
3540
|
-
// LEFT edge lines up with that button (the button now sits at the left end
|
|
3541
|
-
// of the header, so 10px is exactly the header's own left padding).
|
|
3542
|
-
// Open/close animate from the button's center: the animation lives in
|
|
3543
|
-
// the injected stylesheet (classes, not inline animation) so React
|
|
3544
|
-
// re-renders never restart or cancel it — an inline `animation` set on
|
|
3545
|
-
// every render made the entrance invisible and the exit never play.
|
|
3546
|
-
(levelsOpen || levelsClosing) ? react_jsx_runtime.jsx("div", {
|
|
3547
|
-
className: "dqt-levels-pop" + (levelsClosing ? " dqt-levels-pop-closing" : ""),
|
|
3548
|
-
style: {
|
|
3549
|
-
position: "absolute",
|
|
3550
|
-
top: 40,
|
|
3551
|
-
left: 10,
|
|
3552
|
-
zIndex: 30,
|
|
3553
|
-
display: "flex",
|
|
3554
|
-
alignItems: "center",
|
|
3555
|
-
flexWrap: "wrap",
|
|
3556
|
-
gap: 4,
|
|
3557
|
-
padding: 8,
|
|
3558
|
-
background: C.panelBg,
|
|
3559
|
-
border: "1px solid " + C.panelBorder,
|
|
3560
|
-
borderRadius: 10,
|
|
3561
|
-
cornerShape: "round",
|
|
3562
|
-
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.28), 0 2px 6px rgba(0, 0, 0, 0.18)",
|
|
3563
|
-
maxWidth: "calc(100% - 20px)",
|
|
3564
|
-
// the button's center sits 12px inside the popup's left edge
|
|
3565
|
-
transformOrigin: "12px top"
|
|
3566
|
-
},
|
|
3567
|
-
children: [1, 2, 3, 4, 5, 6].map(function (lv) {
|
|
3568
|
-
var active = !!levelSet[lv];
|
|
3569
|
-
return react_jsx_runtime.jsx("button", {
|
|
3570
|
-
onClick: function () { toggleLevel(lv); },
|
|
3571
|
-
title: active ? T("levels.hide") + lv : T("levels.show") + lv,
|
|
3572
|
-
style: {
|
|
3573
|
-
height: 22,
|
|
3574
|
-
padding: "0 7px",
|
|
3575
|
-
border: "none",
|
|
3576
|
-
borderRadius: 6,
|
|
3577
|
-
cornerShape: "round",
|
|
3578
|
-
cursor: "pointer",
|
|
3579
|
-
fontSize: 11,
|
|
3580
|
-
lineHeight: "22px",
|
|
3581
|
-
fontFamily: "inherit",
|
|
3582
|
-
background: active
|
|
3583
|
-
? C.chip
|
|
3584
|
-
: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
3585
|
-
color: active ? C.accent : C.muted,
|
|
3586
|
-
fontWeight: active ? 600 : 400
|
|
3587
|
-
},
|
|
3588
|
-
children: "H" + lv
|
|
3589
|
-
}, "lv" + lv);
|
|
3590
|
-
})
|
|
3591
|
-
}) : null
|
|
4998
|
+
levelsPopDockEl
|
|
3592
4999
|
]
|
|
3593
5000
|
});
|
|
3594
5001
|
|
|
3595
|
-
// ---- edge collapse handle
|
|
3596
|
-
|
|
5002
|
+
// ---- edge collapse handle ------------------------------------------------
|
|
5003
|
+
// It emerges from BEHIND the conversation area's edge and slips back into it, the
|
|
5004
|
+
// same vocabulary the docked panel uses when it collapses: the box is parked
|
|
5005
|
+
// EDGE_W outside the line and clipped at it, so the 12px pill is progressively
|
|
5006
|
+
// revealed as it slides out. A plain fade-in-place made it read as appearing and
|
|
5007
|
+
// vanishing ON the divider line. `open` also drives it (fade + slide home) and
|
|
5008
|
+
// `edgeGone` keeps the element in the tree until that motion has played.
|
|
5009
|
+
var EDGE_W = 26;
|
|
5010
|
+
var edgeShown = !open && handleShown && chatViewActive;
|
|
5011
|
+
var edgeLineLeft = viewport ? viewport.left : 0;
|
|
5012
|
+
var edgeLineRight = viewport ? viewport.right + 52 : 60; // clear of the milestone rail
|
|
5013
|
+
var edgeEl = (!open || !edgeGone) ? react_jsx_runtime.jsx("div", {
|
|
3597
5014
|
style: {
|
|
3598
5015
|
position: "fixed",
|
|
3599
|
-
width:
|
|
5016
|
+
width: EDGE_W,
|
|
3600
5017
|
height: 92,
|
|
3601
5018
|
display: "flex",
|
|
3602
5019
|
alignItems: "center",
|
|
3603
5020
|
justifyContent: "center",
|
|
3604
5021
|
cursor: "pointer",
|
|
3605
|
-
zIndex:
|
|
5022
|
+
zIndex: Z_HANDLE,
|
|
3606
5023
|
// the ratio counts from the BOTTOM (0% = bottom, 100% = top) — at 0.5 this
|
|
3607
|
-
// is the same centred spot the handle always had
|
|
3608
|
-
top
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
5024
|
+
// is the same centred spot the handle always had. The 100% end still lands
|
|
5025
|
+
// exactly on the area's top edge; only the far end is floored, because the
|
|
5026
|
+
// handle is the ONLY way back to a collapsed panel and may never be pushed
|
|
5027
|
+
// off the bottom by a stored rect that is one layout generation behind.
|
|
5028
|
+
top: Math.max(0, Math.min(Math.max(0, window.innerHeight - 100),
|
|
5029
|
+
(viewport
|
|
5030
|
+
? viewport.top + (viewport.height - 92) * handleFromTop
|
|
5031
|
+
: (window.innerHeight - 92) * handleFromTop))) + "px",
|
|
5032
|
+
// clipped at the line it hides behind: parked EDGE_W further out, the whole
|
|
5033
|
+
// box falls inside the clipped strip, so it is invisible without a fade
|
|
5034
|
+
clipPath: dockRight
|
|
5035
|
+
? (edgeShown ? "inset(0 0 0 0)" : "inset(0 " + EDGE_W + "px 0 0)")
|
|
5036
|
+
: (edgeShown ? "inset(0 0 0 0)" : "inset(0 0 0 " + EDGE_W + "px)"),
|
|
5037
|
+
opacity: (chatViewActive && !open) ? 1 : 0,
|
|
5038
|
+
pointerEvents: edgeShown ? "auto" : "none",
|
|
5039
|
+
transition: "left 0.22s " + EASE + ", right 0.22s " + EASE + ", clip-path 0.22s " + EASE + ", opacity 0.18s ease",
|
|
3615
5040
|
...(dockRight
|
|
3616
|
-
? { right:
|
|
3617
|
-
: { left:
|
|
5041
|
+
? { right: edgeShown ? edgeLineRight : edgeLineRight - EDGE_W }
|
|
5042
|
+
: { left: edgeShown ? edgeLineLeft : edgeLineLeft - EDGE_W })
|
|
3618
5043
|
},
|
|
3619
5044
|
title: T("handle.expand"),
|
|
3620
|
-
onClick: function () {
|
|
5045
|
+
onClick: function () { openFromEdge(); },
|
|
3621
5046
|
onMouseEnter: function () { if (edgeRef.current) edgeRef.current.style.opacity = "1"; },
|
|
3622
5047
|
onMouseLeave: function () { if (edgeRef.current) edgeRef.current.style.opacity = "0.5"; },
|
|
3623
5048
|
children: react_jsx_runtime.jsx("div", {
|
|
@@ -3651,6 +5076,111 @@ window.__ModuleLoader__.load({
|
|
|
3651
5076
|
})
|
|
3652
5077
|
}) : null;
|
|
3653
5078
|
|
|
5079
|
+
// where the curtain hangs from: just under the view tabs, with a floor so the
|
|
5080
|
+
// tabs can never eat the whole surface
|
|
5081
|
+
var convTopPx = viewport ? viewport.top : 0;
|
|
5082
|
+
var sheetTopPx = viewport
|
|
5083
|
+
? Math.max(convTopPx, Math.min(sheetStrip ? sheetStrip.bottom : convTopPx, convTopPx + viewport.height - 160))
|
|
5084
|
+
: 0;
|
|
5085
|
+
// The handle sits INSIDE the tab strip, bottom edge on that same line, so no
|
|
5086
|
+
// conversation text can ever run under it. The strip measured 25px tall on this
|
|
5087
|
+
// build, so the hit box fits the band instead of the other way round (a fixed
|
|
5088
|
+
// 26px box poked 3px below the line, straight back into the text it was moved
|
|
5089
|
+
// away from); the 12px pill inside is bottom-aligned, so the visible control
|
|
5090
|
+
// rests ON the line. Falls back to hanging below the line if the strip was never
|
|
5091
|
+
// measured.
|
|
5092
|
+
var HANDLE_BOX_MAX = 26;
|
|
5093
|
+
var stripH = sheetStrip ? Math.max(0, sheetStrip.bottom - sheetStrip.top) : 0;
|
|
5094
|
+
var handleH = sheetStrip ? Math.max(16, Math.min(HANDLE_BOX_MAX, stripH)) : HANDLE_BOX_MAX;
|
|
5095
|
+
var handleTopPx = sheetStrip ? sheetStrip.bottom - handleH : sheetTopPx;
|
|
5096
|
+
|
|
5097
|
+
// ---- curtain handle resting on the view tab strip's line -----------------
|
|
5098
|
+
// A droplet held to the line by surface tension rather than a floating pill: the
|
|
5099
|
+
// silhouette flares out into two concave fillets that meet the strip's lower line
|
|
5100
|
+
// tangentially, and the little triangle says "this drops down". Laid out so its
|
|
5101
|
+
// base sits exactly ON that line (the svg's own bottom edge), centred on the
|
|
5102
|
+
// conversation area — the band above the line is host chrome and never carries
|
|
5103
|
+
// transcript text, which is what made the old handle unreadable.
|
|
5104
|
+
// 140 wide and only 13.5 tall: the reader asked for a wider, flatter droplet
|
|
5105
|
+
// (it started at 92, then 120).
|
|
5106
|
+
var DROP_W = 140;
|
|
5107
|
+
var topHandleEl = (!sheetOpen || !sheetHandleGone) && chatViewActive ? react_jsx_runtime.jsx("div", {
|
|
5108
|
+
className: "dqt-sheet-handle",
|
|
5109
|
+
"data-sheet-handle": "on",
|
|
5110
|
+
style: {
|
|
5111
|
+
position: "fixed",
|
|
5112
|
+
width: DROP_W,
|
|
5113
|
+
height: handleH,
|
|
5114
|
+
display: "flex",
|
|
5115
|
+
alignItems: "flex-end",
|
|
5116
|
+
justifyContent: "center",
|
|
5117
|
+
boxSizing: "border-box",
|
|
5118
|
+
cursor: "pointer",
|
|
5119
|
+
zIndex: Z_HANDLE,
|
|
5120
|
+
top: handleTopPx,
|
|
5121
|
+
transformOrigin: "50% 100%",
|
|
5122
|
+
// The press is SLOW FIRST, THEN FAST, and it is the pop keyframe below read
|
|
5123
|
+
// backwards: `cubic-bezier(0.75, 0, 1, 0.45)` mirrors exactly onto the pop's
|
|
5124
|
+
// `cubic-bezier(0, 0.55, 0.25, 1)`, so the two directions are one motion and
|
|
5125
|
+
// its rewind rather than two lookalike eases. Over the 200ms: 86% of the clock
|
|
5126
|
+
// buys the first 40% of the squash, and the last 40% lands inside ~11ms. The
|
|
5127
|
+
// fade is held back past the press so the squash is never hidden behind it.
|
|
5128
|
+
opacity: sheetOpen ? 0 : 1,
|
|
5129
|
+
transition: "transform 0.2s cubic-bezier(0.75, 0, 1, 0.45), opacity 0.12s ease 0.2s",
|
|
5130
|
+
// centred on the conversation area, and clamped like the side handle: it is
|
|
5131
|
+
// the only entry point to the curtain, so it stays inside the window even
|
|
5132
|
+
// if the stored area rect is one layout generation behind
|
|
5133
|
+
left: Math.max(8, Math.min(Math.max(8, window.innerWidth - DROP_W - 8),
|
|
5134
|
+
viewport
|
|
5135
|
+
? viewport.left + (Math.max(320, window.innerWidth - viewport.left - viewport.right) - DROP_W) / 2
|
|
5136
|
+
: (window.innerWidth - DROP_W) / 2)) + "px"
|
|
5137
|
+
},
|
|
5138
|
+
title: T("sheet.openHandle"),
|
|
5139
|
+
onClick: function () {
|
|
5140
|
+
var el = sheetHandleRef.current;
|
|
5141
|
+
if (el && el.parentElement && el.parentElement.style) el.parentElement.style.transform = "scaleY(0.12)";
|
|
5142
|
+
setSheetHandleGone(false);
|
|
5143
|
+
// The press has to be SEEN before the curtain takes the stage. With the panel
|
|
5144
|
+
// open it already is: the panel's own 300ms tuck comes first. Collapsed, the
|
|
5145
|
+
// curtain used to drop on this very frame and cover the droplet (z 16 over
|
|
5146
|
+
// 12), which is why the press "had no animation" — so here the curtain waits
|
|
5147
|
+
// for the press to play out.
|
|
5148
|
+
if (open) openSheet();
|
|
5149
|
+
else {
|
|
5150
|
+
if (dropOpenRef.current) clearTimeout(dropOpenRef.current);
|
|
5151
|
+
dropOpenRef.current = setTimeout(function () {
|
|
5152
|
+
dropOpenRef.current = null;
|
|
5153
|
+
openSheet();
|
|
5154
|
+
}, DROP_OUT_MS);
|
|
5155
|
+
}
|
|
5156
|
+
},
|
|
5157
|
+
onMouseEnter: function () { if (sheetHandleRef.current) sheetHandleRef.current.style.opacity = "1"; },
|
|
5158
|
+
onMouseLeave: function () { if (sheetHandleRef.current) sheetHandleRef.current.style.opacity = "0.5"; },
|
|
5159
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
5160
|
+
ref: sheetHandleRef,
|
|
5161
|
+
width: DROP_W,
|
|
5162
|
+
height: 17,
|
|
5163
|
+
viewBox: "0 0 140 17",
|
|
5164
|
+
// flush with the box's bottom edge, i.e. exactly on the strip's line
|
|
5165
|
+
style: { display: "block", opacity: 0.5, transition: "opacity 0.25s ease", cursor: "pointer" },
|
|
5166
|
+
children: [
|
|
5167
|
+
react_jsx_runtime.jsx("path", {
|
|
5168
|
+
// base from x=8 to x=132 (124px), dome up at y=3.5, both sides flaring
|
|
5169
|
+
// into concave fillets that become horizontal where they touch the line
|
|
5170
|
+
d: "M8 17 Q30 17 42 9 C50 4.5 58 3.5 70 3.5 C82 3.5 90 4.5 98 9 Q110 17 132 17 Z",
|
|
5171
|
+
fill: "var(--dsw-alias-border-l2, rgba(160,160,160,0.55))"
|
|
5172
|
+
}, "drop"),
|
|
5173
|
+
react_jsx_runtime.jsx("path", {
|
|
5174
|
+
// the side handle's own triangle, turned a quarter turn and taken down a
|
|
5175
|
+
// shade — 14 x 4.5 against its 5.5 x 16 — so it reads as the same
|
|
5176
|
+
// affordance without crowding the smaller droplet it sits in
|
|
5177
|
+
d: "M63 8.5 L77 8.5 L70 13 Z",
|
|
5178
|
+
fill: "var(--dsw-alias-label-secondary, #9aa0ab)"
|
|
5179
|
+
}, "tip")
|
|
5180
|
+
]
|
|
5181
|
+
})
|
|
5182
|
+
}) : null;
|
|
5183
|
+
|
|
3654
5184
|
// ---- hover preview card ------------------------------------------------
|
|
3655
5185
|
// Rendered INSIDE the body portal (position: fixed) so the outline's own
|
|
3656
5186
|
// scroll container can never clip it, and pointer-events: none so it can
|
|
@@ -3699,22 +5229,179 @@ window.__ModuleLoader__.load({
|
|
|
3699
5229
|
color: C.text,
|
|
3700
5230
|
border: "1px solid " + C.panelBorder,
|
|
3701
5231
|
boxShadow: "0 8px 24px rgba(0,0,0,0.28)",
|
|
3702
|
-
|
|
5232
|
+
// above the panel and the curtain — it is a sibling of both in this
|
|
5233
|
+
// portal, and both sit below it in the layer band we claim
|
|
5234
|
+
zIndex: Z_HOVER,
|
|
3703
5235
|
pointerEvents: "none"
|
|
3704
5236
|
},
|
|
3705
5237
|
children: cardChildren
|
|
3706
5238
|
}, "hover-card");
|
|
3707
5239
|
}
|
|
3708
5240
|
|
|
5241
|
+
// ---- the curtain shell ---------------------------------------------------
|
|
5242
|
+
// TWO boxes: an outer clip box that sits at the anchor line with the curtain's
|
|
5243
|
+
// own size, and the curtain inside it, which flies down from above that line
|
|
5244
|
+
// (translateY -100% -> 0) and back up on the way out. The clip is what makes it
|
|
5245
|
+
// emerge from under the view tabs — like the docked panel emerging from behind
|
|
5246
|
+
// the sidebar — so the surface is never seen above the line it hangs from.
|
|
5247
|
+
// The outer box never unmounts: while the curtain is down it is a zero-size,
|
|
5248
|
+
// non-clipping pass-through, so the panel (and the whole list DOM) is never
|
|
5249
|
+
// moved or rebuilt when the curtain opens or closes.
|
|
5250
|
+
var sheetUp = sheetOpen && chatViewActive;
|
|
5251
|
+
var sheetShellEl = react_jsx_runtime.jsx("div", {
|
|
5252
|
+
key: "sheet-shell",
|
|
5253
|
+
className: "dqt-sheet-clip",
|
|
5254
|
+
"data-sheet-shell": sheetUp ? "on" : undefined,
|
|
5255
|
+
style: sheetUp ? {
|
|
5256
|
+
position: "fixed",
|
|
5257
|
+
// hangs from just below the 对话/轨迹/上下文 strip
|
|
5258
|
+
top: sheetTopPx,
|
|
5259
|
+
left: viewport ? viewport.left : 0,
|
|
5260
|
+
width: viewport ? Math.max(320, window.innerWidth - viewport.left - viewport.right) : "100%",
|
|
5261
|
+
// a curtain over the conversation, not the whole screen: the composer and
|
|
5262
|
+
// the newest message stay reachable below it. The height is measured from
|
|
5263
|
+
// the conversation box (not from the curtain's own top) so hanging it below
|
|
5264
|
+
// the tabs does not push its bottom edge down over the composer.
|
|
5265
|
+
height: viewport
|
|
5266
|
+
? Math.max(240, Math.min(760, (viewport.height - Math.max(0, sheetTopPx - viewport.top)) * 0.82))
|
|
5267
|
+
: "70vh",
|
|
5268
|
+
overflow: "hidden",
|
|
5269
|
+
zIndex: Z_SHEET
|
|
5270
|
+
} : {
|
|
5271
|
+
// Nothing while it is down — and in particular no border: a 1px border on a
|
|
5272
|
+
// zero-size box is still a 2px border box, and with the wrapper's overflow
|
|
5273
|
+
// visible that added 2px to the DOCUMENT's scrollable overflow. That raised a
|
|
5274
|
+
// page scrollbar while the curtain was down, so opening it dropped the
|
|
5275
|
+
// scrollbar and widened the whole app by ~8px — which is exactly the small
|
|
5276
|
+
// rightward shift of the conversation the reader saw. A fixed, zero-size,
|
|
5277
|
+
// borderless box contributes nothing, and the fixed-positioned docked panel
|
|
5278
|
+
// inside still resolves against the viewport and is not clipped by it.
|
|
5279
|
+
//
|
|
5280
|
+
// It DOES have to carry the panel's own layer though: a fixed-position
|
|
5281
|
+
// element creates a stacking context even at `z-index: auto`, so with the
|
|
5282
|
+
// shell at auto the docked panel inside it could never paint at its own
|
|
5283
|
+
// z-index — it sat below every positive-z-index element of the app. The
|
|
5284
|
+
// transcript width grips (z 8, a 40px full-height column just outside the
|
|
5285
|
+
// text column) won the hit test INSIDE the open panel: the cursor turned
|
|
5286
|
+
// into col-resize over the outline and a press there resized the
|
|
5287
|
+
// conversation instead of using the panel. Probed in a real browser with
|
|
5288
|
+
// elementFromPoint plus a temporary marker of the same z-index:
|
|
5289
|
+
// shell auto -> at the grip's own column the hit was the grip
|
|
5290
|
+
// shell 14 -> the hit is the outline row, and a 13 marker no longer
|
|
5291
|
+
// covers the panel while a 15 marker still does
|
|
5292
|
+
zIndex: Z_PANEL,
|
|
5293
|
+
position: "fixed",
|
|
5294
|
+
top: 0,
|
|
5295
|
+
left: 0,
|
|
5296
|
+
width: 0,
|
|
5297
|
+
height: 0,
|
|
5298
|
+
overflow: "visible"
|
|
5299
|
+
},
|
|
5300
|
+
children: react_jsx_runtime.jsx("div", {
|
|
5301
|
+
ref: sheetRef,
|
|
5302
|
+
className: sheetUp ? "dqt-sheet dqt-sheet-open" : "dqt-sheet-idle",
|
|
5303
|
+
"data-sheet": sheetUp ? "on" : undefined,
|
|
5304
|
+
style: {
|
|
5305
|
+
width: sheetUp ? "100%" : 0,
|
|
5306
|
+
height: sheetUp ? "100%" : 0,
|
|
5307
|
+
display: "flex",
|
|
5308
|
+
flexDirection: "column",
|
|
5309
|
+
background: C.panelBg,
|
|
5310
|
+
color: C.text,
|
|
5311
|
+
border: sheetUp ? "1px solid " + C.panelBorder : "none",
|
|
5312
|
+
// square where it meets the top edge, since the curtain hangs from there;
|
|
5313
|
+
// no dimming layer and no drop shadow either — the conversation below
|
|
5314
|
+
// stays legible and usable, and the curtain announces itself by flying
|
|
5315
|
+
borderRadius: "0 0 12px 12px",
|
|
5316
|
+
overflow: "hidden"
|
|
5317
|
+
},
|
|
5318
|
+
children: panelEl
|
|
5319
|
+
})
|
|
5320
|
+
});
|
|
5321
|
+
|
|
3709
5322
|
return react_dom.createPortal(
|
|
3710
|
-
react_jsx_runtime.jsx(ErrorBoundary, {
|
|
5323
|
+
react_jsx_runtime.jsx(ErrorBoundary, {
|
|
5324
|
+
children: [sheetShellEl, edgeEl, topHandleEl, hoverEl]
|
|
5325
|
+
}),
|
|
3711
5326
|
document.body
|
|
3712
5327
|
);
|
|
3713
5328
|
}
|
|
3714
5329
|
|
|
5330
|
+
// the "to the section's end" control that every jumpable row (outline entries
|
|
5331
|
+
// and group headers, docked and curtain alike) carries at its far right. It
|
|
5332
|
+
// stays hidden until the row is hovered — the row's own mouseenter/leave
|
|
5333
|
+
// reveal it — and the click is handled once, in the panel's capture phase,
|
|
5334
|
+
// so the row's jump-to-start can never fire alongside it.
|
|
5335
|
+
function renderEndBtn(C, key, idx, turn) {
|
|
5336
|
+
// These markers are not decoration: the row's hover handler looks for
|
|
5337
|
+
// [data-dqt-end] to reveal the control, and the panel's capture phase looks
|
|
5338
|
+
// for it to route the click to the end jump instead of the row's own
|
|
5339
|
+
// jump-to-start. They are deliberately NOT data-jump-key: that attribute
|
|
5340
|
+
// belongs to the rows themselves, and two kinds of element claiming it made
|
|
5341
|
+
// every "find the row" query ambiguous.
|
|
5342
|
+
var attrs = { "data-dqt-end": "1" };
|
|
5343
|
+
if (key) attrs["data-end-key"] = key;
|
|
5344
|
+
if (idx !== undefined && idx !== null) attrs["data-end-idx"] = String(idx);
|
|
5345
|
+
if (turn !== undefined && turn !== null) attrs["data-end-turn"] = String(turn);
|
|
5346
|
+
return react_jsx_runtime.jsx("button", Object.assign({
|
|
5347
|
+
type: "button",
|
|
5348
|
+
tabIndex: -1,
|
|
5349
|
+
title: T("row.end"),
|
|
5350
|
+
onMouseEnter: function (e) {
|
|
5351
|
+
e.currentTarget.style.background = C.hover;
|
|
5352
|
+
e.currentTarget.style.color = C.text;
|
|
5353
|
+
},
|
|
5354
|
+
onMouseLeave: function (e) {
|
|
5355
|
+
e.currentTarget.style.background = "transparent";
|
|
5356
|
+
e.currentTarget.style.color = C.muted;
|
|
5357
|
+
},
|
|
5358
|
+
style: {
|
|
5359
|
+
position: "absolute",
|
|
5360
|
+
right: 4,
|
|
5361
|
+
top: "50%",
|
|
5362
|
+
transform: "translateY(-50%)",
|
|
5363
|
+
width: 20,
|
|
5364
|
+
height: 20,
|
|
5365
|
+
display: "flex",
|
|
5366
|
+
alignItems: "center",
|
|
5367
|
+
justifyContent: "center",
|
|
5368
|
+
padding: 0,
|
|
5369
|
+
border: "none",
|
|
5370
|
+
borderRadius: 999,
|
|
5371
|
+
cornerShape: "round",
|
|
5372
|
+
background: "transparent",
|
|
5373
|
+
color: C.muted,
|
|
5374
|
+
// hover-only: the row raises this to 1 and the transition fades it in
|
|
5375
|
+
// and out, so the control never appears or vanishes with a snap
|
|
5376
|
+
opacity: 0,
|
|
5377
|
+
cursor: "pointer",
|
|
5378
|
+
transition: "opacity 0.22s ease, background 0.18s ease, color 0.18s ease"
|
|
5379
|
+
},
|
|
5380
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
5381
|
+
width: 12,
|
|
5382
|
+
height: 12,
|
|
5383
|
+
viewBox: "0 0 12 12",
|
|
5384
|
+
style: { display: "block" },
|
|
5385
|
+
children: react_jsx_runtime.jsx("path", {
|
|
5386
|
+
// an arrow down onto a floor line: "go to where this section stops"
|
|
5387
|
+
d: "M6 1.5V8M2.8 5.2 6 8.4l3.2-3.2M2 11h8",
|
|
5388
|
+
fill: "none",
|
|
5389
|
+
stroke: "currentColor",
|
|
5390
|
+
strokeWidth: 1.5,
|
|
5391
|
+
strokeLinecap: "round",
|
|
5392
|
+
strokeLinejoin: "round"
|
|
5393
|
+
})
|
|
5394
|
+
})
|
|
5395
|
+
}, attrs), "end-" + (key || "") + "-" + (idx !== undefined && idx !== null ? idx : turn));
|
|
5396
|
+
}
|
|
5397
|
+
|
|
3715
5398
|
function renderItem(n, depth, jump, C, uid, extra) {
|
|
3716
5399
|
var hasChildren = !!(n.children && n.children.length);
|
|
3717
5400
|
var path = extra && extra.paths ? extra.paths[headingId(n)] : null;
|
|
5401
|
+
// Wide (curtain) rows put the same information on ONE line: an H-level chip,
|
|
5402
|
+
// the title, and the section's opening text out to the right — the space the
|
|
5403
|
+
// docked panel does not have. The narrow panel keeps title-over-subtitle.
|
|
5404
|
+
var wide = !!(extra && extra.wide);
|
|
3718
5405
|
var hoverInfo = {
|
|
3719
5406
|
title: n.title,
|
|
3720
5407
|
sub: n.sub || "",
|
|
@@ -3722,57 +5409,124 @@ window.__ModuleLoader__.load({
|
|
|
3722
5409
|
meta: [n.time, path ? path.path : ""].filter(Boolean).join(" · "),
|
|
3723
5410
|
ghost: false
|
|
3724
5411
|
};
|
|
3725
|
-
var rows
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
5412
|
+
var rows;
|
|
5413
|
+
if (wide) {
|
|
5414
|
+
var context = n.preview || n.sub || "";
|
|
5415
|
+
rows = [react_jsx_runtime.jsx("div", {
|
|
5416
|
+
style: { display: "flex", alignItems: "baseline", gap: 8, minWidth: 0 },
|
|
5417
|
+
children: [
|
|
5418
|
+
react_jsx_runtime.jsx("span", {
|
|
5419
|
+
style: { flex: "none", width: 22, fontSize: 11.5, fontWeight: 600, letterSpacing: 0.2, color: C.muted, opacity: 0.65 },
|
|
5420
|
+
children: "H" + n.level
|
|
5421
|
+
}, uid + "-lv"),
|
|
5422
|
+
react_jsx_runtime.jsx("span", {
|
|
5423
|
+
style: { flex: "0 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" },
|
|
5424
|
+
children: n.title
|
|
5425
|
+
}),
|
|
5426
|
+
// basis 0 + grow: the small text only ever takes the space the title
|
|
5427
|
+
// leaves over, so a long title is never squeezed by its own context
|
|
5428
|
+
context ? react_jsx_runtime.jsx("span", {
|
|
5429
|
+
style: {
|
|
5430
|
+
flex: "1 1 0",
|
|
5431
|
+
minWidth: 0,
|
|
5432
|
+
fontSize: 13,
|
|
5433
|
+
color: C.muted,
|
|
5434
|
+
opacity: 0.68,
|
|
5435
|
+
textAlign: "right",
|
|
5436
|
+
overflow: "hidden",
|
|
5437
|
+
textOverflow: "ellipsis"
|
|
5438
|
+
},
|
|
5439
|
+
children: context
|
|
5440
|
+
}) : null
|
|
5441
|
+
]
|
|
5442
|
+
}, uid + "-l")];
|
|
5443
|
+
} else {
|
|
5444
|
+
rows = [
|
|
5445
|
+
react_jsx_runtime.jsx("div", {
|
|
5446
|
+
style: { display: "flex", alignItems: "center", gap: 2, minWidth: 0 },
|
|
5447
|
+
children: react_jsx_runtime.jsx("span", {
|
|
5448
|
+
style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" },
|
|
5449
|
+
children: n.title
|
|
5450
|
+
})
|
|
5451
|
+
}, uid + "-l")
|
|
5452
|
+
];
|
|
5453
|
+
// Subtitle = the section's first sentence. This is what makes two headings
|
|
5454
|
+
// with the SAME text distinguishable without hovering ("which one is this?").
|
|
5455
|
+
if (n.sub) {
|
|
5456
|
+
rows.push(react_jsx_runtime.jsx("div", {
|
|
5457
|
+
style: {
|
|
5458
|
+
fontSize: 10,
|
|
5459
|
+
lineHeight: "13px",
|
|
5460
|
+
color: C.muted,
|
|
5461
|
+
opacity: 0.75,
|
|
5462
|
+
whiteSpace: "nowrap",
|
|
5463
|
+
overflow: "hidden",
|
|
5464
|
+
textOverflow: "ellipsis"
|
|
5465
|
+
},
|
|
5466
|
+
children: n.sub
|
|
5467
|
+
}, uid + "-s"));
|
|
5468
|
+
}
|
|
3749
5469
|
}
|
|
5470
|
+
// the section's own "jump to its end" control, revealed while the row is
|
|
5471
|
+
// hovered; the reserved right padding keeps the title/subtitle/context
|
|
5472
|
+
// clear of it so nothing ever runs underneath the button
|
|
5473
|
+
rows.push(renderEndBtn(C, n.key, n.idx));
|
|
5474
|
+
// The hover highlight is its own layer rather than the row's own background:
|
|
5475
|
+
// the row box runs to its right edge, which is exactly where the end control's
|
|
5476
|
+
// round backdrop appears, so the two boxes used to sit flush against each other
|
|
5477
|
+
// (and on a long row the highlight ran underneath the button). This layer stops
|
|
5478
|
+
// END_ZONE short of that edge, leaving a clear gap.
|
|
5479
|
+
rows.unshift(react_jsx_runtime.jsx("div", {
|
|
5480
|
+
"data-dqt-hl": "1",
|
|
5481
|
+
style: {
|
|
5482
|
+
position: "absolute",
|
|
5483
|
+
left: 0,
|
|
5484
|
+
top: 0,
|
|
5485
|
+
bottom: 0,
|
|
5486
|
+
right: END_ZONE,
|
|
5487
|
+
borderRadius: 6,
|
|
5488
|
+
background: "transparent",
|
|
5489
|
+
transition: "background 0.15s ease",
|
|
5490
|
+
pointerEvents: "none"
|
|
5491
|
+
}
|
|
5492
|
+
}, uid + "-hl"));
|
|
3750
5493
|
return react_jsx_runtime.jsx(
|
|
3751
5494
|
"div",
|
|
3752
5495
|
{
|
|
3753
5496
|
onClick: function () { jump(n.key, n.idx); },
|
|
3754
5497
|
onMouseEnter: function (e) {
|
|
3755
|
-
e.currentTarget.
|
|
5498
|
+
var hl = e.currentTarget.querySelector("[data-dqt-hl]");
|
|
5499
|
+
if (hl) hl.style.background = C.hover;
|
|
5500
|
+
var b = e.currentTarget.querySelector("[data-dqt-end]");
|
|
5501
|
+
if (b) b.style.opacity = "1";
|
|
3756
5502
|
if (extra && extra.hoverStart) extra.hoverStart(hoverInfo, e.currentTarget);
|
|
3757
5503
|
},
|
|
3758
5504
|
onMouseLeave: function (e) {
|
|
3759
|
-
e.currentTarget.
|
|
5505
|
+
var hl = e.currentTarget.querySelector("[data-dqt-hl]");
|
|
5506
|
+
if (hl) hl.style.background = "transparent";
|
|
5507
|
+
var b = e.currentTarget.querySelector("[data-dqt-end]");
|
|
5508
|
+
if (b) b.style.opacity = "0";
|
|
3760
5509
|
if (extra && extra.hoverEnd) extra.hoverEnd();
|
|
3761
5510
|
},
|
|
3762
5511
|
"data-jump-key": n.key,
|
|
3763
5512
|
"data-jump-idx": n.idx !== undefined ? String(n.idx) : "0",
|
|
5513
|
+
// keyboard navigation walks the ORDER of these rows inside the list
|
|
5514
|
+
"data-nav-row": "1",
|
|
3764
5515
|
style: {
|
|
3765
5516
|
// no fixed height: a row grows by one line when it has a subtitle
|
|
5517
|
+
// (narrow layout) — in wide mode it is one line by construction
|
|
3766
5518
|
display: "block",
|
|
3767
|
-
|
|
3768
|
-
|
|
5519
|
+
position: "relative",
|
|
5520
|
+
padding: wide ? "4px 8px" : "2px 6px",
|
|
5521
|
+
paddingRight: END_ZONE,
|
|
5522
|
+
paddingLeft: (hasChildren ? (wide ? 4 : 2) : (wide ? 8 : 6)) + (n.level - 1) * (wide ? 16 : 12),
|
|
3769
5523
|
margin: "1px 0",
|
|
3770
5524
|
borderRadius: 6,
|
|
3771
5525
|
cursor: "pointer",
|
|
3772
|
-
fontSize: n.level <= 2 ? 13 : 12,
|
|
5526
|
+
fontSize: n.level <= 2 ? (wide ? 16.5 : 13) : (wide ? 15.5 : 12),
|
|
3773
5527
|
color: n.level <= 2 ? C.text : C.muted,
|
|
3774
5528
|
fontWeight: n.level <= 2 ? 600 : 400,
|
|
3775
|
-
lineHeight: "18px",
|
|
5529
|
+
lineHeight: wide ? "26px" : "18px",
|
|
3776
5530
|
whiteSpace: "nowrap",
|
|
3777
5531
|
overflow: "hidden"
|
|
3778
5532
|
},
|
|
@@ -3813,43 +5567,84 @@ window.__ModuleLoader__.load({
|
|
|
3813
5567
|
|
|
3814
5568
|
function renderResultRow(r, i, q, C, onRow, isActive, extra) {
|
|
3815
5569
|
var fuzzy = !!(extra && extra.fuzzy);
|
|
5570
|
+
// the curtain's rows are wide: title, snippet and meta share ONE line (title
|
|
5571
|
+
// left, context middle-right, meta far right) instead of stacking three
|
|
5572
|
+
// half-empty lines. The docked panel keeps the stacked form.
|
|
5573
|
+
var wide = !!(extra && extra.wide);
|
|
3816
5574
|
var meta = [];
|
|
3817
5575
|
if (r.ghost) meta.push(T("turn.unloaded"));
|
|
3818
5576
|
if (r.failed) meta.push(T("turn.failed"));
|
|
3819
5577
|
if (r.time) meta.push(r.time);
|
|
3820
5578
|
if (r.path) meta.push(r.path);
|
|
3821
5579
|
if (r.count > 1) meta.push("×" + r.count);
|
|
3822
|
-
var children
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
5580
|
+
var children;
|
|
5581
|
+
if (wide) {
|
|
5582
|
+
children = [
|
|
5583
|
+
react_jsx_runtime.jsx("span", {
|
|
5584
|
+
style: {
|
|
5585
|
+
flex: "0 1 auto",
|
|
5586
|
+
minWidth: 0,
|
|
5587
|
+
fontSize: 15.5,
|
|
5588
|
+
fontWeight: r.level > 0 && r.level <= 2 ? 600 : 400,
|
|
5589
|
+
color: r.level > 0 ? C.text : C.muted,
|
|
5590
|
+
whiteSpace: "nowrap",
|
|
5591
|
+
overflow: "hidden",
|
|
5592
|
+
textOverflow: "ellipsis"
|
|
5593
|
+
},
|
|
5594
|
+
children: hitSpans(r.title, q, fuzzy)
|
|
5595
|
+
}, "t" + i),
|
|
5596
|
+
r.snippet ? react_jsx_runtime.jsx("span", {
|
|
5597
|
+
style: {
|
|
5598
|
+
flex: "1 1 0",
|
|
5599
|
+
minWidth: 0,
|
|
5600
|
+
fontSize: 13,
|
|
5601
|
+
color: C.muted,
|
|
5602
|
+
opacity: 0.85,
|
|
5603
|
+
textAlign: "right",
|
|
5604
|
+
whiteSpace: "nowrap",
|
|
5605
|
+
overflow: "hidden",
|
|
5606
|
+
textOverflow: "ellipsis"
|
|
5607
|
+
},
|
|
5608
|
+
children: hitSpans(r.snippet, q, fuzzy)
|
|
5609
|
+
}, "s" + i) : null,
|
|
5610
|
+
meta.length ? react_jsx_runtime.jsx("span", {
|
|
5611
|
+
style: { flex: "none", fontSize: 12, color: C.muted, opacity: 0.7, whiteSpace: "nowrap" },
|
|
5612
|
+
children: meta.join(" · ")
|
|
5613
|
+
}, "m" + i) : null
|
|
5614
|
+
];
|
|
5615
|
+
} else {
|
|
5616
|
+
children = [
|
|
5617
|
+
react_jsx_runtime.jsx("div", {
|
|
5618
|
+
style: {
|
|
5619
|
+
fontSize: 12,
|
|
5620
|
+
fontWeight: r.level > 0 && r.level <= 2 ? 600 : 400,
|
|
5621
|
+
color: r.level > 0 ? C.text : C.muted,
|
|
5622
|
+
whiteSpace: "nowrap",
|
|
5623
|
+
overflow: "hidden",
|
|
5624
|
+
textOverflow: "ellipsis"
|
|
5625
|
+
},
|
|
5626
|
+
children: hitSpans(r.title, q, fuzzy)
|
|
5627
|
+
}, "t" + i)
|
|
5628
|
+
];
|
|
5629
|
+
if (r.snippet) {
|
|
5630
|
+
children.push(react_jsx_runtime.jsx("div", {
|
|
5631
|
+
style: {
|
|
5632
|
+
fontSize: 11,
|
|
5633
|
+
color: C.muted,
|
|
5634
|
+
marginTop: 1,
|
|
5635
|
+
whiteSpace: "nowrap",
|
|
5636
|
+
overflow: "hidden",
|
|
5637
|
+
textOverflow: "ellipsis"
|
|
5638
|
+
},
|
|
5639
|
+
children: hitSpans(r.snippet, q, fuzzy)
|
|
5640
|
+
}, "s" + i));
|
|
5641
|
+
}
|
|
5642
|
+
if (meta.length) {
|
|
5643
|
+
children.push(react_jsx_runtime.jsx("div", {
|
|
5644
|
+
style: { fontSize: 10, color: C.muted, opacity: 0.8, marginTop: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
|
|
5645
|
+
children: meta.join(" · ")
|
|
5646
|
+
}, "m" + i));
|
|
5647
|
+
}
|
|
3853
5648
|
}
|
|
3854
5649
|
return react_jsx_runtime.jsx("div", {
|
|
3855
5650
|
// clicking a row makes that hit the CURRENT one: it highlights the
|
|
@@ -3873,13 +5668,18 @@ window.__ModuleLoader__.load({
|
|
|
3873
5668
|
if (extra && extra.hoverEnd) extra.hoverEnd();
|
|
3874
5669
|
},
|
|
3875
5670
|
"data-result-idx": i,
|
|
5671
|
+
"data-nav-row": "1",
|
|
3876
5672
|
title: r.path || r.title,
|
|
3877
5673
|
style: {
|
|
3878
|
-
padding: "4px 6px",
|
|
5674
|
+
padding: wide ? "6px 10px" : "4px 6px",
|
|
3879
5675
|
margin: "1px 0",
|
|
3880
5676
|
borderRadius: 6,
|
|
3881
5677
|
cursor: "pointer",
|
|
3882
5678
|
minWidth: 0,
|
|
5679
|
+
// wide: one line, three parts — level chip / title, context, meta
|
|
5680
|
+
display: wide ? "flex" : "block",
|
|
5681
|
+
alignItems: wide ? "baseline" : undefined,
|
|
5682
|
+
gap: wide ? 10 : undefined,
|
|
3883
5683
|
background: isActive ? C.chip : "transparent",
|
|
3884
5684
|
transition: "background 0.15s ease"
|
|
3885
5685
|
},
|
|
@@ -3901,13 +5701,86 @@ window.__ModuleLoader__.load({
|
|
|
3901
5701
|
return out;
|
|
3902
5702
|
}
|
|
3903
5703
|
|
|
5704
|
+
// cross-session list: the HOST's index over the other sessions. One row per hit —
|
|
5705
|
+
// the hit session's title, the host's bounded snippet, and the scope note — and
|
|
5706
|
+
// clicking a row hands that session to openCrossHit. No hover card here: the
|
|
5707
|
+
// panel has no section text for a session it has not loaded.
|
|
5708
|
+
function renderCrossResults(cross, q, C, titleOf, onOpen) {
|
|
5709
|
+
var line = function (text, key, color, size) {
|
|
5710
|
+
return react_jsx_runtime.jsx("div", {
|
|
5711
|
+
style: {
|
|
5712
|
+
padding: "10px 8px",
|
|
5713
|
+
fontSize: size || 12,
|
|
5714
|
+
color: color || C.muted,
|
|
5715
|
+
textAlign: "center",
|
|
5716
|
+
lineHeight: 1.5,
|
|
5717
|
+
overflowWrap: "anywhere"
|
|
5718
|
+
},
|
|
5719
|
+
children: text
|
|
5720
|
+
}, key);
|
|
5721
|
+
};
|
|
5722
|
+
var out = [
|
|
5723
|
+
react_jsx_runtime.jsx("div", {
|
|
5724
|
+
key: "cross-note",
|
|
5725
|
+
style: { padding: "2px 8px 6px", fontSize: 10, color: C.muted, opacity: 0.8, lineHeight: 1.45 },
|
|
5726
|
+
children: T("search.cross.note")
|
|
5727
|
+
})
|
|
5728
|
+
];
|
|
5729
|
+
if (cross.phase === "loading") {
|
|
5730
|
+
out.push(line(T("search.cross.loading"), "cross-loading"));
|
|
5731
|
+
return out;
|
|
5732
|
+
}
|
|
5733
|
+
if (cross.phase === "error") {
|
|
5734
|
+
out.push(line(T("search.cross.error") + cross.error, "cross-error", C.error || C.muted));
|
|
5735
|
+
return out;
|
|
5736
|
+
}
|
|
5737
|
+
if (!cross.rows || cross.rows.length === 0) {
|
|
5738
|
+
out.push(line(T("search.cross.empty"), "cross-empty"));
|
|
5739
|
+
if (cross.hidden > 0) {
|
|
5740
|
+
out.push(line(Tp("search.cross.hidden", { n: cross.hidden }), "cross-hidden", C.muted, 10));
|
|
5741
|
+
}
|
|
5742
|
+
return out;
|
|
5743
|
+
}
|
|
5744
|
+
var rows = cross.rows;
|
|
5745
|
+
var open = function (i) { onOpen(rows[i].sessionId); };
|
|
5746
|
+
for (var i = 0; i < rows.length; i++) {
|
|
5747
|
+
out.push(renderResultRow({
|
|
5748
|
+
title: titleOf(rows[i].sessionId),
|
|
5749
|
+
snippet: rows[i].snippet,
|
|
5750
|
+
level: 0,
|
|
5751
|
+
path: "",
|
|
5752
|
+
time: "",
|
|
5753
|
+
count: 1
|
|
5754
|
+
}, i, q, C, open, false, null));
|
|
5755
|
+
}
|
|
5756
|
+
if (cross.more) {
|
|
5757
|
+
out.push(react_jsx_runtime.jsx("div", {
|
|
5758
|
+
key: "cross-more",
|
|
5759
|
+
style: { padding: "8px 8px 2px", fontSize: 10, color: C.muted, opacity: 0.8, textAlign: "center" },
|
|
5760
|
+
children: T("search.cross.more")
|
|
5761
|
+
}));
|
|
5762
|
+
}
|
|
5763
|
+
// Hits the panel refuses to offer (archived / unlisted / subagent) are counted,
|
|
5764
|
+
// not silently dropped: "why is my hit missing" must have an answer on screen.
|
|
5765
|
+
if (cross.hidden > 0) {
|
|
5766
|
+
out.push(line(Tp("search.cross.hidden", { n: cross.hidden }), "cross-hidden", C.muted, 10));
|
|
5767
|
+
}
|
|
5768
|
+
return out;
|
|
5769
|
+
}
|
|
5770
|
+
|
|
3904
5771
|
// a group's header (the time row): clicking it jumps to the TOP OF THE
|
|
3905
5772
|
// MODEL'S REPLY for that turn — the first assistant step — which also makes
|
|
3906
5773
|
// heading-less turns jumpable, since they only have this row.
|
|
3907
5774
|
// Implemented as a function (not an inline closure in a loop) so each
|
|
3908
5775
|
// header captures its own (g, gi) — the var-in-loop closure bug would
|
|
3909
5776
|
// otherwise make every header jump to the last group.
|
|
3910
|
-
function renderGroupHeader(g, gi, jump, C, isActive) {
|
|
5777
|
+
function renderGroupHeader(g, gi, jump, C, isActive, extra) {
|
|
5778
|
+
// questions-only view and the wide curtain both give the header more room: it
|
|
5779
|
+
// grows (11px -> 14px, 18px -> 24px) and its prompt stops being a footnote
|
|
5780
|
+
// (0.75 -> 0.92 opacity on the body colour). The current-turn box is untouched
|
|
5781
|
+
// either way, and every header in a given view still shares ONE style.
|
|
5782
|
+
var big = !!(extra && (extra.questionsOnly || extra.wide));
|
|
5783
|
+
var wideBand = !!(extra && extra.wide && !extra.questionsOnly);
|
|
3911
5784
|
// a turn that ended in a failure has no reply to land on: its terminal row IS
|
|
3912
5785
|
// the interesting position, so the header lands there (and says so in the tip)
|
|
3913
5786
|
var replyKey = (g.msgs && g.msgs.length > 0) ? g.msgs[0].key : (g.failure ? g.failure.key : "");
|
|
@@ -3919,14 +5792,32 @@ window.__ModuleLoader__.load({
|
|
|
3919
5792
|
if (g.userKey && findRow(g.userKey)) { jump(g.userKey); return; }
|
|
3920
5793
|
if (g.headings.length > 0) jump(g.headings[0].key, g.headings[0].idx);
|
|
3921
5794
|
};
|
|
5795
|
+
// the group's own "jump to its end": the turn's last row (the failure row for
|
|
5796
|
+
// a failed turn, else the last reply). Revealed while the header is hovered.
|
|
5797
|
+
var lastKey = (g.msgs && g.msgs.length > 0) ? g.msgs[g.msgs.length - 1].key
|
|
5798
|
+
: (g.failure ? g.failure.key : "");
|
|
3922
5799
|
return react_jsx_runtime.jsx("div", {
|
|
5800
|
+
onMouseEnter: function (e) {
|
|
5801
|
+
var b = e.currentTarget.querySelector("[data-dqt-end]");
|
|
5802
|
+
if (b) b.style.opacity = "1";
|
|
5803
|
+
},
|
|
5804
|
+
onMouseLeave: function (e) {
|
|
5805
|
+
var b = e.currentTarget.querySelector("[data-dqt-end]");
|
|
5806
|
+
if (b) b.style.opacity = "0";
|
|
5807
|
+
},
|
|
3923
5808
|
style: {
|
|
3924
5809
|
// ONE row geometry for every group header, whether or not the turn has
|
|
3925
5810
|
// headings: same box, same 18px rhythm, same left inset, so the time
|
|
3926
5811
|
// column of a heading-less turn lines up with all the others. Only the
|
|
3927
5812
|
// type emphasis differs (weight + colour, see the label below).
|
|
3928
|
-
padding: "1px 4px 2px",
|
|
3929
|
-
|
|
5813
|
+
padding: big ? (wideBand ? "3px 4px 4px" : "2px 4px 3px") : "1px 4px 2px",
|
|
5814
|
+
// the label is capped by this, not by a maxWidth guess: the row reserves the
|
|
5815
|
+
// end control's zone, so a long prompt's hover box ends END_ZONE from the row
|
|
5816
|
+
// edge — a real gap before the button's round backdrop, which sits 24px in.
|
|
5817
|
+
// (With `max-width: calc(100% - 32px)` the percentage resolved somewhere else
|
|
5818
|
+
// and a forced-long header still came within 2px of the circle.)
|
|
5819
|
+
paddingRight: END_ZONE,
|
|
5820
|
+
height: big ? (wideBand ? 24 : 22) : 18,
|
|
3930
5821
|
display: "flex",
|
|
3931
5822
|
alignItems: "center",
|
|
3932
5823
|
minWidth: 0,
|
|
@@ -3957,6 +5848,7 @@ window.__ModuleLoader__.load({
|
|
|
3957
5848
|
react_jsx_runtime.jsx("span", {
|
|
3958
5849
|
onClick: jumpToTurn,
|
|
3959
5850
|
title: (g.msgs && g.msgs.length > 0) ? T("turn.jumpReply") : (g.failure ? T("turn.jumpFailed") : T("turn.jumpTurn")),
|
|
5851
|
+
"data-nav-row": "1",
|
|
3960
5852
|
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
3961
5853
|
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
3962
5854
|
style: {
|
|
@@ -3966,11 +5858,11 @@ window.__ModuleLoader__.load({
|
|
|
3966
5858
|
// with the rest of the list (same size, weight, colour, opacity). The only
|
|
3967
5859
|
// thing that marks a group is the shared "current group" tint, which lives
|
|
3968
5860
|
// on the group box — never on the label.
|
|
3969
|
-
fontSize: 11,
|
|
5861
|
+
fontSize: big ? (wideBand ? 14 : 12.5) : 11,
|
|
3970
5862
|
fontWeight: 400,
|
|
3971
|
-
color: C.muted,
|
|
5863
|
+
color: big ? C.text : C.muted,
|
|
3972
5864
|
cursor: "pointer",
|
|
3973
|
-
padding: "1px 5px",
|
|
5865
|
+
padding: big ? "1px 5px" : "1px 5px",
|
|
3974
5866
|
borderRadius: 4,
|
|
3975
5867
|
background: "transparent",
|
|
3976
5868
|
transition: "background 0.15s ease",
|
|
@@ -3978,6 +5870,8 @@ window.__ModuleLoader__.load({
|
|
|
3978
5870
|
alignItems: "center",
|
|
3979
5871
|
gap: 5,
|
|
3980
5872
|
minWidth: 0,
|
|
5873
|
+
// the row's paddingRight already reserves the end control's zone, so the
|
|
5874
|
+
// label simply fills what is left: its hover box can never reach the circle
|
|
3981
5875
|
maxWidth: "100%",
|
|
3982
5876
|
overflow: "hidden"
|
|
3983
5877
|
},
|
|
@@ -3986,7 +5880,7 @@ window.__ModuleLoader__.load({
|
|
|
3986
5880
|
g.userText ? react_jsx_runtime.jsx("span", {
|
|
3987
5881
|
style: {
|
|
3988
5882
|
fontWeight: 400,
|
|
3989
|
-
opacity: 0.75,
|
|
5883
|
+
opacity: big ? 0.92 : 0.75,
|
|
3990
5884
|
overflow: "hidden",
|
|
3991
5885
|
textOverflow: "ellipsis",
|
|
3992
5886
|
whiteSpace: "nowrap",
|
|
@@ -3995,7 +5889,8 @@ window.__ModuleLoader__.load({
|
|
|
3995
5889
|
children: g.userText
|
|
3996
5890
|
}) : null
|
|
3997
5891
|
]
|
|
3998
|
-
})
|
|
5892
|
+
}),
|
|
5893
|
+
renderEndBtn(C, lastKey, undefined, g.turn)
|
|
3999
5894
|
]
|
|
4000
5895
|
}, "g-h-" + gi);
|
|
4001
5896
|
}
|
|
@@ -4040,6 +5935,7 @@ window.__ModuleLoader__.load({
|
|
|
4040
5935
|
},
|
|
4041
5936
|
"data-group-idx": gi,
|
|
4042
5937
|
"data-ghost-turn": g.turn,
|
|
5938
|
+
"data-nav-row": "1",
|
|
4043
5939
|
title: T("turn.loadTip"),
|
|
4044
5940
|
style: {
|
|
4045
5941
|
padding: "3px 6px",
|
|
@@ -4131,8 +6027,10 @@ window.__ModuleLoader__.load({
|
|
|
4131
6027
|
// every group gets the SAME header, whether or not this turn produced any
|
|
4132
6028
|
// markdown headings — a heading-less turn is just a group with no rows, not
|
|
4133
6029
|
// a second kind of entry (`rows` only decides what gets listed below it)
|
|
4134
|
-
items.push(renderGroupHeader(g, gi, jump, C, isActive));
|
|
4135
|
-
|
|
6030
|
+
items.push(renderGroupHeader(g, gi, jump, C, isActive, extra));
|
|
6031
|
+
// questions-only reading: the header IS the content, so its rows are left out
|
|
6032
|
+
// entirely (the header itself grows and brightens for that job)
|
|
6033
|
+
if (!extra.questionsOnly) items.push(renderNodes(rows, 0, jump, C, "g" + gi, extra));
|
|
4136
6034
|
// the failure is the turn's LAST event, so its row sits under the rows
|
|
4137
6035
|
if (g.failure) items.push(renderFailureRow(g, gi, jump, C));
|
|
4138
6036
|
}
|
|
@@ -4358,6 +6256,7 @@ window.__ModuleLoader__.load({
|
|
|
4358
6256
|
field("handle", T("settings.handle"), T("settings.handle.tip"), handleControl, false),
|
|
4359
6257
|
field("fuzzy", T("settings.fuzzy"), "", onoff(prefs.fuzzy, function (v) { commit({ fuzzy: v }); }), true),
|
|
4360
6258
|
field("hover", T("settings.hover"), "", onoff(prefs.hover, function (v) { commit({ hover: v }); }), true),
|
|
6259
|
+
field("remember", T("settings.remember"), T("settings.remember.tip"), onoff(prefs.remember, function (v) { commit({ remember: v }); }), true),
|
|
4361
6260
|
field("debug", T("settings.debug"), T("settings.debug.tip"), onoff(prefs.debug, function (v) { commit({ debug: v }); }), true, true)
|
|
4362
6261
|
];
|
|
4363
6262
|
return react_jsx_runtime.jsxs("li", {
|
|
@@ -4459,6 +6358,16 @@ window.__ModuleLoader__.load({
|
|
|
4459
6358
|
} catch (e) {
|
|
4460
6359
|
return null;
|
|
4461
6360
|
}
|
|
6361
|
+
},
|
|
6362
|
+
// The workspace registry owns the archive set the sidebar filters with
|
|
6363
|
+
// (list.getSnapshot().archivedSessionIds). Cross-session hits are filtered
|
|
6364
|
+
// by the SAME rule, so a hit the sidebar would hide is never offered here.
|
|
6365
|
+
workspaces: function () {
|
|
6366
|
+
try {
|
|
6367
|
+
return typeof ctx.get === "function" ? (ctx.get("workspaces") || null) : null;
|
|
6368
|
+
} catch (e) {
|
|
6369
|
+
return null;
|
|
6370
|
+
}
|
|
4462
6371
|
}
|
|
4463
6372
|
};
|
|
4464
6373
|
|
|
@@ -4534,6 +6443,43 @@ window.__ModuleLoader__.load({
|
|
|
4534
6443
|
"@keyframes dqt-toast-out{from{opacity:1;transform:none}to{opacity:0;transform:none}}" +
|
|
4535
6444
|
".dqt-toast{animation:dqt-toast-in 0.2s cubic-bezier(0.22, 0.9, 0.3, 1) both}" +
|
|
4536
6445
|
".dqt-toast-closing{animation:dqt-toast-out 0.42s linear both}" +
|
|
6446
|
+
// questions-only switch: fade the list out, swap the mode while it is
|
|
6447
|
+
// invisible, fade it back in (the two lists never blend mid-swap)
|
|
6448
|
+
".dqt-mode-out{animation:dqt-fade-out 0.15s linear both}" +
|
|
6449
|
+
".dqt-mode-in{animation:dqt-fade-in 0.26s cubic-bezier(0.22, 0.9, 0.3, 1) both}" +
|
|
6450
|
+
// the curtain FLIES DOWN from above the line it hangs from and flies back up
|
|
6451
|
+
// on the way out — the same vocabulary as the docked panel sliding in from its
|
|
6452
|
+
// edge. The clip box it lives in (see the portal) is what keeps it from being
|
|
6453
|
+
// seen above that line, so it reads as emerging from under the view tabs.
|
|
6454
|
+
"@keyframes dqt-sheet-in{from{transform:translateY(-100%)}to{transform:translateY(0)}}" +
|
|
6455
|
+
"@keyframes dqt-sheet-out{from{transform:translateY(0)}to{transform:translateY(-100%)}}" +
|
|
6456
|
+
".dqt-sheet-open{animation:dqt-sheet-in 0.46s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform}" +
|
|
6457
|
+
".dqt-sheet-closing{animation:dqt-sheet-out 0.3s cubic-bezier(0.4, 0, 1, 1) both;pointer-events:none;will-change:transform}" +
|
|
6458
|
+
"@media (prefers-reduced-motion: reduce){.dqt-sheet-open,.dqt-sheet-closing{animation:none}}" +
|
|
6459
|
+
// The curtain handle is a droplet resting on the view-tab strip's lower line:
|
|
6460
|
+
// its silhouette flares into two concave fillets at the line (surface tension,
|
|
6461
|
+
// not a detached pill). It grows out of the line when it appears — after a
|
|
6462
|
+
// retract as much as on first paint — and the click squeezes it flat against
|
|
6463
|
+
// the line before the curtain takes over (see the element's own transform).
|
|
6464
|
+
"@keyframes dqt-handle-pop{from{transform:scaleY(0.06)}to{transform:scaleY(1)}}" +
|
|
6465
|
+
".dqt-sheet-handle{transform-origin:50% 100%}" +
|
|
6466
|
+
// coming back it is the press read backwards: FAST OUT OF THE LINE — 74% of the
|
|
6467
|
+
// easing is done 30ms in — and then the rest settles over the remaining 170ms.
|
|
6468
|
+
// `cubic-bezier(0, 0.55, 0.25, 1)` is exactly `cubic-bezier(0.75, 0, 1, 0.45)`
|
|
6469
|
+
// mirrored, which is the curve the press uses.
|
|
6470
|
+
".dqt-sheet-handle>svg{animation:dqt-handle-pop 0.2s cubic-bezier(0, 0.55, 0.25, 1) both;transform-origin:50% 100%}" +
|
|
6471
|
+
"@media (prefers-reduced-motion: reduce){.dqt-sheet-handle>svg{animation:none}}" +
|
|
6472
|
+
// keyboard cursor: an INSET outline plus a whisper of fill, so it never
|
|
6473
|
+
// reflows the row and stays distinct from the reading position's blue box
|
|
6474
|
+
// (which keeps the 3px left accent bar and the pastel fill)
|
|
6475
|
+
".dqt-nav{box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary, #4f8cff);background:rgba(79,140,255,0.10);border-radius:6px}" +
|
|
6476
|
+
".dqt-nav:focus{outline:none}" +
|
|
6477
|
+
// the list itself never draws a focus ring — the cursor row is the indicator
|
|
6478
|
+
".dqt-list:focus-visible{outline:none}" +
|
|
6479
|
+
// screen-reader-only live region
|
|
6480
|
+
".dqt-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}" +
|
|
6481
|
+
// the fade is decoration: a reader who asked for less motion gets the swap
|
|
6482
|
+
"@media (prefers-reduced-motion: reduce){.dqt-mode-out,.dqt-mode-in{animation:none}}" +
|
|
4537
6483
|
// exit: starts moving immediately (fast attack), so the dismiss feels snappy
|
|
4538
6484
|
".dqt-levels-pop-closing{animation:dqt-pop-out 0.14s cubic-bezier(0.2, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
|
|
4539
6485
|
// Hide the native scrollbar entirely (no arrow buttons, no grey bar) —
|