dsh-quick-toc 0.5.1 → 0.6.1
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 +34 -0
- package/CHANGELOG.md +34 -0
- package/README.en.md +27 -19
- package/README.md +23 -15
- package/lib/client.js +1173 -221
- package/lib/index.js +69 -6
- package/package.json +8 -2
package/lib/client.js
CHANGED
|
@@ -55,7 +55,6 @@ window.__ModuleLoader__.load({
|
|
|
55
55
|
"outline.bottom": "回到底部",
|
|
56
56
|
"turn.jumpReply": "跳转到该回合的模型回答开头",
|
|
57
57
|
"turn.jumpTurn": "跳转到该回合开头",
|
|
58
|
-
"turn.loading": "正在加载这个回合…",
|
|
59
58
|
"turn.unloaded": "未加载",
|
|
60
59
|
"turn.loadTip": "点击加载这个回合并跳转过去",
|
|
61
60
|
"turn.failed": "请求失败",
|
|
@@ -65,17 +64,502 @@ window.__ModuleLoader__.load({
|
|
|
65
64
|
"preview.truncated": "预览由宿主提供,可能被截断",
|
|
66
65
|
"resize.w": "拖拽调整宽度",
|
|
67
66
|
"resize.h": "拖拽调整高度",
|
|
68
|
-
"resize.wh": "拖拽同时调整宽高"
|
|
67
|
+
"resize.wh": "拖拽同时调整宽高",
|
|
68
|
+
"settings.desc": "对话大纲面板的语言与显示偏好,保存在 DSH 的设置里。",
|
|
69
|
+
"settings.language": "语言",
|
|
70
|
+
"settings.language.tip": "「跟随宿主」使用 DSH 当前的语言",
|
|
71
|
+
"settings.lang.auto": "跟随宿主",
|
|
72
|
+
"settings.lang.zh": "中文",
|
|
73
|
+
"settings.lang.en": "English",
|
|
74
|
+
"settings.dock": "默认停靠边缘",
|
|
75
|
+
"settings.dock.left": "左侧",
|
|
76
|
+
"settings.dock.right": "右侧",
|
|
77
|
+
"settings.levels": "显示的标题层级",
|
|
78
|
+
"settings.zoom": "面板缩放",
|
|
79
|
+
"settings.zoom.tip": "文字、图标与按钮按比例缩放,面板本身的尺寸不变",
|
|
80
|
+
"settings.on": "开启",
|
|
81
|
+
"settings.off": "关闭",
|
|
82
|
+
"settings.fuzzy": "模糊搜索",
|
|
83
|
+
"settings.hover": "悬停预览卡片",
|
|
84
|
+
"settings.debug": "在控制台打印诊断日志",
|
|
85
|
+
"settings.debug.tip": "开启后控制台会打印一行挂载诊断(宿主能力与跳转加载器状态)",
|
|
86
|
+
"settings.overridden": "已自定义",
|
|
87
|
+
"settings.resetField": "重置",
|
|
88
|
+
"settings.readOnly": "当前页面无法写入 DSH 设置,面板暂时使用本浏览器保存的偏好。"
|
|
89
|
+
},
|
|
90
|
+
en: {
|
|
91
|
+
"panel.title": "Conversation Outline",
|
|
92
|
+
"error.panel": "dsh-quick-toc panel error: ",
|
|
93
|
+
"handle.dragY": "Drag to move the panel",
|
|
94
|
+
"handle.dockLeft": "Move to the left",
|
|
95
|
+
"handle.dockRight": "Move to the right",
|
|
96
|
+
"handle.collapse": "Collapse",
|
|
97
|
+
"handle.expand": "Expand the outline",
|
|
98
|
+
"levels.tip": "Heading level filter",
|
|
99
|
+
"levels.show": "Show H",
|
|
100
|
+
"levels.hide": "Hide H",
|
|
101
|
+
"search.open": "Search headings",
|
|
102
|
+
"search.word": "Search",
|
|
103
|
+
"search.tail": ", Enter to jump…",
|
|
104
|
+
"search.scope.title": "Title",
|
|
105
|
+
"search.scope.full": "Full text",
|
|
106
|
+
"search.scope.tipTitle": "Titles only. Click for full-text search",
|
|
107
|
+
"search.scope.tipFull": "Full text. Click for titles only",
|
|
108
|
+
"search.fuzzy": "Fuzzy",
|
|
109
|
+
"search.fuzzy.tipOn": "Fuzzy matching is on: a few other characters may sit between the keywords, so more matches hit",
|
|
110
|
+
"search.fuzzy.tipOff": "Fuzzy matching is off: only contiguous text matches. Click to turn it on",
|
|
111
|
+
"search.empty": "No matches",
|
|
112
|
+
"search.hintMore": "Scroll up to load earlier messages",
|
|
113
|
+
"search.hintOldest": "This is the earliest message",
|
|
114
|
+
"hint.bottom": "You have reached the end",
|
|
115
|
+
"outline.bottom": "Back to the newest entry",
|
|
116
|
+
"turn.jumpReply": "Jump to the start of this turn's model reply",
|
|
117
|
+
"turn.jumpTurn": "Jump to the start of this turn",
|
|
118
|
+
"turn.unloaded": "Not loaded",
|
|
119
|
+
"turn.loadTip": "Click to load this turn and jump to it",
|
|
120
|
+
"turn.failed": "Request failed",
|
|
121
|
+
"turn.jumpFailed": "Jump to this turn's error",
|
|
122
|
+
// NB: "time.beforeYesterday" stays Chinese-only on purpose — English has no
|
|
123
|
+
// natural word for it that beats the date, so anything older than yesterday
|
|
124
|
+
// carries the numeric date. Yesterday itself reads as the word in both.
|
|
125
|
+
"time.yesterday": "yesterday",
|
|
126
|
+
"preview.truncated": "Preview comes from the host and may be truncated",
|
|
127
|
+
"resize.w": "Drag to resize the width",
|
|
128
|
+
"resize.h": "Drag to resize the height",
|
|
129
|
+
"resize.wh": "Drag to resize width and height",
|
|
130
|
+
"settings.desc": "Language and display preferences for the conversation outline panel, kept in DSH's settings.",
|
|
131
|
+
"settings.language": "Language",
|
|
132
|
+
"settings.language.tip": "\"Follow the host\" uses whatever language DSH currently runs in",
|
|
133
|
+
"settings.lang.auto": "Follow the host",
|
|
134
|
+
"settings.lang.zh": "中文",
|
|
135
|
+
"settings.lang.en": "English",
|
|
136
|
+
"settings.dock": "Default docked edge",
|
|
137
|
+
"settings.dock.left": "Left",
|
|
138
|
+
"settings.dock.right": "Right",
|
|
139
|
+
"settings.levels": "Heading levels shown",
|
|
140
|
+
"settings.zoom": "Panel scale",
|
|
141
|
+
"settings.zoom.tip": "Scales the text, icons and buttons; the panel's own size is unchanged",
|
|
142
|
+
"settings.on": "On",
|
|
143
|
+
"settings.off": "Off",
|
|
144
|
+
"settings.fuzzy": "Fuzzy search",
|
|
145
|
+
"settings.hover": "Hover preview card",
|
|
146
|
+
"settings.debug": "Print the mount diagnostic to the console",
|
|
147
|
+
"settings.debug.tip": "The console then prints one mount line (host capabilities and jump-loader state)",
|
|
148
|
+
"settings.overridden": "Customized",
|
|
149
|
+
"settings.resetField": "Reset",
|
|
150
|
+
"settings.readOnly": "This page cannot write DSH settings; the panel is using the preferences saved in this browser."
|
|
69
151
|
}
|
|
70
152
|
};
|
|
71
|
-
// The dictionary the current render reads.
|
|
72
|
-
// the
|
|
73
|
-
//
|
|
74
|
-
|
|
153
|
+
// The dictionary the current render reads. `T` prefers the HOST translation for
|
|
154
|
+
// the active language when one is bound (that is what makes "follow the host"
|
|
155
|
+
// work and what makes a language switch in Settings live), and falls back to the
|
|
156
|
+
// built-in table so the panel still speaks a language when the host has no
|
|
157
|
+
// locale service. Kept as a module-level binding so the module-scope render
|
|
158
|
+
// helpers (renderItem/renderResultRow/...) can localise without threading a `t`
|
|
159
|
+
// argument through every call.
|
|
160
|
+
var activeLang = "zh"; // resolved language: "zh" | "en"
|
|
161
|
+
var langPref = "auto"; // user preference: "auto" | "zh" | "en"
|
|
162
|
+
var hostTranslate = null; // (key) => string | undefined, bound by apply()
|
|
163
|
+
var builtinDict = function () { return DICTS[activeLang] || DICTS.zh; };
|
|
164
|
+
// The English table is complete except for the two Chinese-only relative days (see
|
|
165
|
+
// the note in it), so the Chinese table is the last stop before the key itself —
|
|
166
|
+
// a raw key like "turn.unloaded" must never reach the screen.
|
|
75
167
|
function T(key) {
|
|
76
|
-
|
|
168
|
+
if (langPref === "auto" && typeof hostTranslate === "function") {
|
|
169
|
+
var viaHost = hostTranslate(key);
|
|
170
|
+
if (typeof viaHost === "string" && viaHost !== "") return viaHost;
|
|
171
|
+
}
|
|
172
|
+
var s = builtinDict()[key];
|
|
173
|
+
if (s === undefined && activeLang !== "zh") s = DICTS.zh[key];
|
|
77
174
|
return s === undefined ? key : s;
|
|
78
175
|
}
|
|
176
|
+
// The host reports the language it resolved for us; `auto` follows it, an explicit
|
|
177
|
+
// preference wins, and an unknown host language falls back to Chinese.
|
|
178
|
+
function setLanguage(pref, hostLang) {
|
|
179
|
+
langPref = pref === "zh" || pref === "en" ? pref : "auto";
|
|
180
|
+
activeLang = langPref === "auto" ? (hostLang === "en" ? "en" : "zh") : langPref;
|
|
181
|
+
return activeLang;
|
|
182
|
+
}
|
|
183
|
+
// Which language the HOST is running in. The bound translate function is the only
|
|
184
|
+
// signal a plugin gets, so it is probed with one key whose two translations differ
|
|
185
|
+
// (both are registered below, so the answer is exact rather than a guess).
|
|
186
|
+
function hostLanguage() {
|
|
187
|
+
if (typeof hostTranslate !== "function") return null;
|
|
188
|
+
try {
|
|
189
|
+
return hostTranslate("panel.title") === DICTS.en["panel.title"] ? "en" : "zh";
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- preferences ----------
|
|
196
|
+
// ONE module-level home for the panel's preferences, shared by the panel and by
|
|
197
|
+
// the plugin-configuration card, so a change on either side is live on the other.
|
|
198
|
+
//
|
|
199
|
+
// Two layers sit behind it. The Host settings document (the namespace the host
|
|
200
|
+
// half registers) is authoritative on a loopback page — that is what the card in
|
|
201
|
+
// Settings → Plugins → Plugin configuration edits, and what follows the account
|
|
202
|
+
// across browsers. localStorage is the mirror this browser falls back to: DSH
|
|
203
|
+
// marks settings read-only on a non-loopback page, and there the panel keeps
|
|
204
|
+
// working exactly as 0.5.x did. Position and size (y/w/h) are local-only on
|
|
205
|
+
// purpose — they describe THIS screen, not the account.
|
|
206
|
+
var NAMESPACE = "dsh-quick-toc";
|
|
207
|
+
// Fields backed by the Host settings document; everything else is localStorage.
|
|
208
|
+
var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, zoom: 1, fuzzy: 1, hover: 1, debug: 1 };
|
|
209
|
+
// The schema defaults (mirrored here for the card's per-field reset and for the
|
|
210
|
+
// one-time import of a 0.5.x install's stored values).
|
|
211
|
+
var DEFAULTS = {
|
|
212
|
+
dock: "left", y: 0, w: 0, h: 0,
|
|
213
|
+
levels: [1, 2, 3, 4, 5, 6],
|
|
214
|
+
zoom: 1,
|
|
215
|
+
fuzzy: false, hover: true, lang: "auto", debug: false
|
|
216
|
+
};
|
|
217
|
+
var PREF_KEYS = {
|
|
218
|
+
dock: "dsh-quick-toc.dock.v2",
|
|
219
|
+
y: "dsh-quick-toc.panelY.v1",
|
|
220
|
+
w: "dsh-quick-toc.panelW.v1",
|
|
221
|
+
h: "dsh-quick-toc.panelH.v1",
|
|
222
|
+
levels: "dsh-quick-toc.levels.v1",
|
|
223
|
+
zoom: "dsh-quick-toc.zoom.v1",
|
|
224
|
+
fuzzy: "dsh-quick-toc.fuzzy.v1",
|
|
225
|
+
hover: "dsh-quick-toc.hover.v1",
|
|
226
|
+
lang: "dsh-quick-toc.lang.v1",
|
|
227
|
+
debug: "dsh-quick-toc.debug"
|
|
228
|
+
};
|
|
229
|
+
// Legacy keys from the renamed plugin (dsh-dagang -> dsh-contents -> dsh-quick-toc).
|
|
230
|
+
var PREF_LEGACY = {
|
|
231
|
+
dock: ["dsh-contents.dock.v2", "dsh-dagang.dock.v2"],
|
|
232
|
+
y: ["dsh-contents.panelY.v1", "dsh-dagang.panelY.v1"]
|
|
233
|
+
};
|
|
234
|
+
var OLD_MAX_LEVEL_KEY = "dsh-quick-toc.maxLevel.v1";
|
|
235
|
+
var LEVELS_ALL = [1, 2, 3, 4, 5, 6];
|
|
236
|
+
var prefsStore = (function () {
|
|
237
|
+
var hasOwn = function (obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); };
|
|
238
|
+
var read = function (key, legacy) {
|
|
239
|
+
try {
|
|
240
|
+
var v = localStorage.getItem(key);
|
|
241
|
+
if (v !== null) return v;
|
|
242
|
+
var list = legacy || [];
|
|
243
|
+
for (var i = 0; i < list.length; i++) {
|
|
244
|
+
var old = localStorage.getItem(list[i]);
|
|
245
|
+
if (old !== null) { localStorage.setItem(key, old); return old; }
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {}
|
|
248
|
+
return null;
|
|
249
|
+
};
|
|
250
|
+
var parseLevelList = function (arr) {
|
|
251
|
+
if (!Array.isArray(arr)) return null;
|
|
252
|
+
var out = [];
|
|
253
|
+
for (var i = 0; i < arr.length; i++) {
|
|
254
|
+
var lv = Number(arr[i]);
|
|
255
|
+
if (lv >= 1 && lv <= 6 && out.indexOf(lv) < 0) out.push(lv);
|
|
256
|
+
}
|
|
257
|
+
if (out.length > 0) { out.sort(); return out; }
|
|
258
|
+
return null;
|
|
259
|
+
};
|
|
260
|
+
var parseLevels = function (raw) {
|
|
261
|
+
if (raw === null) return null;
|
|
262
|
+
try { return parseLevelList(JSON.parse(raw)); } catch (e) { return null; }
|
|
263
|
+
};
|
|
264
|
+
var loadLocal = function () {
|
|
265
|
+
var rawLevels = read(PREF_KEYS.levels);
|
|
266
|
+
if (rawLevels === null) {
|
|
267
|
+
// pre-0.4.0 stored a single max level instead of a set
|
|
268
|
+
try {
|
|
269
|
+
var max = Number(read(OLD_MAX_LEVEL_KEY));
|
|
270
|
+
if (isFinite(max) && max >= 1) {
|
|
271
|
+
var grown = [];
|
|
272
|
+
for (var lv = 1; lv <= Math.min(6, max); lv++) grown.push(lv);
|
|
273
|
+
rawLevels = JSON.stringify(grown);
|
|
274
|
+
}
|
|
275
|
+
} catch (e) {}
|
|
276
|
+
}
|
|
277
|
+
var dockRaw = read(PREF_KEYS.dock, PREF_LEGACY.dock);
|
|
278
|
+
var yRaw = read(PREF_KEYS.y, PREF_LEGACY.y);
|
|
279
|
+
var wRaw = read(PREF_KEYS.w);
|
|
280
|
+
var hRaw = read(PREF_KEYS.h);
|
|
281
|
+
var langRaw = read(PREF_KEYS.lang);
|
|
282
|
+
var zoomRaw = read(PREF_KEYS.zoom); // NB: absent (null) means 1 — Number(null) is 0!
|
|
283
|
+
return {
|
|
284
|
+
dock: dockRaw === "right" ? "right" : "left",
|
|
285
|
+
// 0 means "the panel's own default" (see PANEL_WIDTH / auto height)
|
|
286
|
+
y: yRaw !== null && isFinite(Number(yRaw)) ? Number(yRaw) : 0,
|
|
287
|
+
w: wRaw !== null && isFinite(Number(wRaw)) && Number(wRaw) >= 180 ? Number(wRaw) : 0,
|
|
288
|
+
h: hRaw !== null && isFinite(Number(hRaw)) && Number(hRaw) >= 160 ? Number(hRaw) : 0,
|
|
289
|
+
levels: parseLevels(rawLevels) || LEVELS_ALL.slice(),
|
|
290
|
+
zoom: zoomRaw !== null && isFinite(Number(zoomRaw)) ? Math.min(2, Math.max(0.5, Math.round(Number(zoomRaw) * 100) / 100)) : 1,
|
|
291
|
+
fuzzy: read(PREF_KEYS.fuzzy) === "1",
|
|
292
|
+
// absent means "on": the hover card predates this switch
|
|
293
|
+
hover: read(PREF_KEYS.hover) !== "0",
|
|
294
|
+
lang: langRaw === "zh" || langRaw === "en" ? langRaw : "auto",
|
|
295
|
+
debug: read(PREF_KEYS.debug) === "1"
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
var write = function (patch) {
|
|
299
|
+
var map = {
|
|
300
|
+
dock: function (v) { return v; },
|
|
301
|
+
y: function (v) { return String(v); },
|
|
302
|
+
w: function (v) { return String(v); },
|
|
303
|
+
h: function (v) { return String(v); },
|
|
304
|
+
levels: function (v) { return JSON.stringify(v); },
|
|
305
|
+
zoom: function (v) { return String(v); },
|
|
306
|
+
fuzzy: function (v) { return v ? "1" : "0"; },
|
|
307
|
+
hover: function (v) { return v ? "1" : "0"; },
|
|
308
|
+
lang: function (v) { return v; },
|
|
309
|
+
debug: function (v) { return v ? "1" : "0"; }
|
|
310
|
+
};
|
|
311
|
+
for (var key in patch) {
|
|
312
|
+
if (!hasOwn(patch, key) || !map[key]) continue;
|
|
313
|
+
try { localStorage.setItem(PREF_KEYS[key], map[key](patch[key])); } catch (e) {}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
var listeners = [];
|
|
317
|
+
var localValues = null;
|
|
318
|
+
var hostScope = null; // the bound client settings scope, or null
|
|
319
|
+
var hostSnap = null; // its last snapshot: {status, value, user, writable, revision}
|
|
320
|
+
var values = null;
|
|
321
|
+
var valuesVersion = -1;
|
|
322
|
+
// The host layer arrives schema-validated, but the overlay still normalises so a
|
|
323
|
+
// half-written document can never hand the panel a wrong type.
|
|
324
|
+
var hostOverlay = function (snap) {
|
|
325
|
+
if (!snap || snap.status !== "ready" || typeof snap.value !== "object" || snap.value === null) return null;
|
|
326
|
+
var v = snap.value;
|
|
327
|
+
var levels = parseLevelList(v.levels);
|
|
328
|
+
var zoomNum = Number(v.zoom);
|
|
329
|
+
return {
|
|
330
|
+
dock: v.dock === "right" ? "right" : "left",
|
|
331
|
+
lang: v.lang === "zh" || v.lang === "en" ? v.lang : "auto",
|
|
332
|
+
levels: levels || LEVELS_ALL.slice(),
|
|
333
|
+
zoom: isFinite(zoomNum) ? Math.min(2, Math.max(0.5, Math.round(zoomNum * 100) / 100)) : 1,
|
|
334
|
+
fuzzy: v.fuzzy === true,
|
|
335
|
+
hover: v.hover !== false,
|
|
336
|
+
debug: v.debug === true
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
var compute = function () {
|
|
340
|
+
var local = localValues || (localValues = loadLocal());
|
|
341
|
+
var over = hostOverlay(hostSnap);
|
|
342
|
+
if (!over) return local;
|
|
343
|
+
return {
|
|
344
|
+
dock: over.dock, y: local.y, w: local.w, h: local.h,
|
|
345
|
+
levels: over.levels, zoom: over.zoom, fuzzy: over.fuzzy, hover: over.hover,
|
|
346
|
+
lang: over.lang, debug: over.debug
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
var notify = function () {
|
|
350
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
351
|
+
try { listeners[i](store.get()); } catch (e) {}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
// Fold write operations into the cached snapshot so the next read is already the
|
|
355
|
+
// new value; the host's own answer replaces it when the write settles.
|
|
356
|
+
var foldOps = function (snap, ops) {
|
|
357
|
+
if (!snap || typeof snap.value !== "object" || snap.value === null) return snap;
|
|
358
|
+
var value = Object.assign({}, snap.value);
|
|
359
|
+
var user = snap.user && typeof snap.user === "object" ? Object.assign({}, snap.user) : {};
|
|
360
|
+
for (var i = 0; i < ops.length; i++) {
|
|
361
|
+
var field = ops[i].path[0];
|
|
362
|
+
value[field] = ops[i].value;
|
|
363
|
+
user[field] = ops[i].value;
|
|
364
|
+
}
|
|
365
|
+
return Object.assign({}, snap, { value: value, user: user });
|
|
366
|
+
};
|
|
367
|
+
var foldUnset = function (snap, field) {
|
|
368
|
+
if (!snap || typeof snap.value !== "object" || snap.value === null) return snap;
|
|
369
|
+
var value = Object.assign({}, snap.value); delete value[field];
|
|
370
|
+
var user = snap.user && typeof snap.user === "object" ? Object.assign({}, snap.user) : {}; delete user[field];
|
|
371
|
+
return Object.assign({}, snap, { value: value, user: user });
|
|
372
|
+
};
|
|
373
|
+
var sendHost = function (ops) {
|
|
374
|
+
if (!ops.length || !hostScope || !hostSnap || hostSnap.writable !== true) return;
|
|
375
|
+
hostSnap = foldOps(hostSnap, ops);
|
|
376
|
+
var settle = function () {
|
|
377
|
+
if (!hostScope) return;
|
|
378
|
+
hostSnap = hostScope.getSnapshot();
|
|
379
|
+
store.version++;
|
|
380
|
+
notify();
|
|
381
|
+
};
|
|
382
|
+
try {
|
|
383
|
+
var p = hostScope.mutate(ops);
|
|
384
|
+
if (p && typeof p.then === "function") p.then(settle, settle);
|
|
385
|
+
} catch (e) { settle(); }
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* One-time import: a 0.5.x install kept every preference in localStorage. The
|
|
389
|
+
* first time the host layer answers, hand it any STORED value that differs from
|
|
390
|
+
* the schema defaults — but only while the user layer is still empty, so a
|
|
391
|
+
* preference already configured through the card is never stomped. The local
|
|
392
|
+
* keys stay: they remain this browser's fallback on non-loopback pages.
|
|
393
|
+
*/
|
|
394
|
+
var importLegacy = function () {
|
|
395
|
+
if (!hostSnap || hostSnap.status !== "ready" || hostSnap.writable !== true) return;
|
|
396
|
+
if (hostSnap.user && typeof hostSnap.user === "object" && Object.keys(hostSnap.user).length > 0) return;
|
|
397
|
+
var local = localValues || loadLocal(); // the stored layer, NOT the merged view
|
|
398
|
+
var ops = [];
|
|
399
|
+
if (local.dock !== DEFAULTS.dock) ops.push({ op: "set", path: ["dock"], value: local.dock });
|
|
400
|
+
if (local.lang !== DEFAULTS.lang) ops.push({ op: "set", path: ["lang"], value: local.lang });
|
|
401
|
+
if (JSON.stringify(local.levels) !== JSON.stringify(DEFAULTS.levels)) ops.push({ op: "set", path: ["levels"], value: local.levels.slice() });
|
|
402
|
+
if (local.zoom !== DEFAULTS.zoom) ops.push({ op: "set", path: ["zoom"], value: local.zoom });
|
|
403
|
+
if (local.fuzzy !== DEFAULTS.fuzzy) ops.push({ op: "set", path: ["fuzzy"], value: local.fuzzy });
|
|
404
|
+
if (local.hover !== DEFAULTS.hover) ops.push({ op: "set", path: ["hover"], value: local.hover });
|
|
405
|
+
if (local.debug !== DEFAULTS.debug) ops.push({ op: "set", path: ["debug"], value: local.debug });
|
|
406
|
+
sendHost(ops);
|
|
407
|
+
};
|
|
408
|
+
var store = {
|
|
409
|
+
version: 0,
|
|
410
|
+
get: function () {
|
|
411
|
+
if (values === null || valuesVersion !== store.version) {
|
|
412
|
+
values = compute();
|
|
413
|
+
valuesVersion = store.version;
|
|
414
|
+
}
|
|
415
|
+
return values;
|
|
416
|
+
},
|
|
417
|
+
/** Merge a patch: route it to the host document and/or the mirror, then tell every subscriber (panel + card). */
|
|
418
|
+
set: function (patch) {
|
|
419
|
+
var current = store.get();
|
|
420
|
+
var next = {};
|
|
421
|
+
var changed = false;
|
|
422
|
+
for (var key in current) {
|
|
423
|
+
if (!hasOwn(current, key)) continue;
|
|
424
|
+
next[key] = hasOwn(patch, key) ? patch[key] : current[key];
|
|
425
|
+
if (key === "levels") {
|
|
426
|
+
if (JSON.stringify(next[key]) !== JSON.stringify(current[key])) changed = true;
|
|
427
|
+
} else if (next[key] !== current[key]) changed = true;
|
|
428
|
+
}
|
|
429
|
+
if (!changed) return current;
|
|
430
|
+
var ops = [];
|
|
431
|
+
for (var k in patch) {
|
|
432
|
+
if (!hasOwn(patch, k)) continue;
|
|
433
|
+
if (HOST_FIELDS[k]) ops.push({ op: "set", path: [k], value: patch[k] });
|
|
434
|
+
}
|
|
435
|
+
sendHost(ops);
|
|
436
|
+
write(patch); // the localStorage mirror (and the local-only y/w/h)
|
|
437
|
+
store.version++;
|
|
438
|
+
notify();
|
|
439
|
+
return store.get();
|
|
440
|
+
},
|
|
441
|
+
/**
|
|
442
|
+
* Clear one field back to its base layer (the card's per-field reset): the
|
|
443
|
+
* host entry is removed — the schema default takes over and the "customized"
|
|
444
|
+
* badge goes away — and the mirror records the default for non-loopback pages.
|
|
445
|
+
*/
|
|
446
|
+
unset: function (field) {
|
|
447
|
+
// the mirror FIRST: clearing the host entry empties the user layer, and the
|
|
448
|
+
// one-time import must not read a stale mirror value and push it right back
|
|
449
|
+
var mirror = {};
|
|
450
|
+
mirror[field] = field === "levels" ? DEFAULTS.levels.slice() : DEFAULTS[field];
|
|
451
|
+
write(mirror);
|
|
452
|
+
if (HOST_FIELDS[field] && hostScope && hostSnap && hostSnap.writable === true) {
|
|
453
|
+
hostSnap = foldUnset(hostSnap, field);
|
|
454
|
+
var settle = function () {
|
|
455
|
+
if (!hostScope) return;
|
|
456
|
+
hostSnap = hostScope.getSnapshot();
|
|
457
|
+
store.version++;
|
|
458
|
+
notify();
|
|
459
|
+
};
|
|
460
|
+
try {
|
|
461
|
+
var p = hostScope.unset(field);
|
|
462
|
+
if (p && typeof p.then === "function") p.then(settle, settle);
|
|
463
|
+
} catch (e) { settle(); }
|
|
464
|
+
}
|
|
465
|
+
store.version++;
|
|
466
|
+
notify();
|
|
467
|
+
return store.get();
|
|
468
|
+
},
|
|
469
|
+
subscribe: function (fn) {
|
|
470
|
+
listeners.push(fn);
|
|
471
|
+
return function () {
|
|
472
|
+
var at = listeners.indexOf(fn);
|
|
473
|
+
if (at >= 0) listeners.splice(at, 1);
|
|
474
|
+
};
|
|
475
|
+
},
|
|
476
|
+
/** The raw host snapshot for surfaces that need more than the merged values (the card's badges). */
|
|
477
|
+
host: function () { return hostSnap; },
|
|
478
|
+
/**
|
|
479
|
+
* Adopt the client settings scope: its snapshot becomes the authoritative layer
|
|
480
|
+
* for the host-backed fields, and its changes flow through the store like any
|
|
481
|
+
* other write.
|
|
482
|
+
*/
|
|
483
|
+
attachHost: function (scope) {
|
|
484
|
+
hostScope = scope;
|
|
485
|
+
hostSnap = scope.getSnapshot();
|
|
486
|
+
store.version++;
|
|
487
|
+
notify();
|
|
488
|
+
scope.subscribe(function () {
|
|
489
|
+
hostSnap = scope.getSnapshot();
|
|
490
|
+
store.version++;
|
|
491
|
+
notify();
|
|
492
|
+
importLegacy();
|
|
493
|
+
});
|
|
494
|
+
importLegacy();
|
|
495
|
+
},
|
|
496
|
+
/**
|
|
497
|
+
* Re-read localStorage and tell every subscriber. Used when another tab writes
|
|
498
|
+
* one of these keys (a `storage` event) — the cached snapshot would otherwise
|
|
499
|
+
* keep the stale value for the rest of this page's life.
|
|
500
|
+
*/
|
|
501
|
+
reload: function () {
|
|
502
|
+
localValues = null;
|
|
503
|
+
store.version++;
|
|
504
|
+
var next = store.get();
|
|
505
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
506
|
+
try { listeners[i](next); } catch (e) {}
|
|
507
|
+
}
|
|
508
|
+
return next;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
return store;
|
|
512
|
+
})();
|
|
513
|
+
// Read the live preferences from a component: the store is versioned so a plain
|
|
514
|
+
// state counter is enough to re-render on every change.
|
|
515
|
+
function usePrefs() {
|
|
516
|
+
var box = react.useState(prefsStore.version);
|
|
517
|
+
react.useEffect(function () {
|
|
518
|
+
return prefsStore.subscribe(function () { box[1](prefsStore.version); });
|
|
519
|
+
}, []);
|
|
520
|
+
return prefsStore.get();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ---- timestamp helpers (module scope: the stamp formatter is used by the
|
|
524
|
+
// component AND by module-scope render helpers) ----
|
|
525
|
+
// Accepts ms / seconds epochs and ISO strings; null when unreadable.
|
|
526
|
+
function toDateOf(t) {
|
|
527
|
+
if (t === undefined || t === null) return null;
|
|
528
|
+
if (typeof t === "string") {
|
|
529
|
+
var d0 = new Date(t);
|
|
530
|
+
return isNaN(d0.getTime()) ? null : d0;
|
|
531
|
+
}
|
|
532
|
+
var n = Number(t);
|
|
533
|
+
if (!isFinite(n) || n <= 0) return null;
|
|
534
|
+
if (n < 1e12) n = n * 1000; // epoch seconds -> ms
|
|
535
|
+
var d = new Date(n);
|
|
536
|
+
return isNaN(d.getTime()) ? null : d;
|
|
537
|
+
}
|
|
538
|
+
function pad2Of(n) { return ("0" + n).slice(-2); }
|
|
539
|
+
function startOfDayOf(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); }
|
|
540
|
+
function fmtNumericDate(d) {
|
|
541
|
+
return pad2Of(d.getFullYear() % 100) + "-" + pad2Of(d.getMonth() + 1) + "-" + pad2Of(d.getDate());
|
|
542
|
+
}
|
|
543
|
+
// A stamp the reader can place in time. Yesterday reads as a word in both
|
|
544
|
+
// languages; Chinese also names the day before, while English falls straight
|
|
545
|
+
// back to the numeric date for anything older (today is always just the clock).
|
|
546
|
+
function fmtStampOf(t) {
|
|
547
|
+
var d = toDateOf(t);
|
|
548
|
+
if (!d) return "";
|
|
549
|
+
var clock = pad2Of(d.getHours()) + ":" + pad2Of(d.getMinutes());
|
|
550
|
+
var days = Math.round((startOfDayOf(d) - startOfDayOf(new Date())) / 86400000);
|
|
551
|
+
if (days >= 0) return clock; // today (a future skew also reads as today)
|
|
552
|
+
if (days === -1) return T("time.yesterday") + " " + clock;
|
|
553
|
+
if (days === -2 && activeLang === "zh") return T("time.beforeYesterday") + " " + clock;
|
|
554
|
+
return fmtNumericDate(d) + " " + clock;
|
|
555
|
+
}
|
|
556
|
+
// The jump in flight (null when none). A newer jump retires the previous one,
|
|
557
|
+
// and the auto-follow stays out of the way while one is running.
|
|
558
|
+
var glide = null;
|
|
559
|
+
// Set once a jump has been seen handed back already finished: from then on the
|
|
560
|
+
// glide is drawn frame by frame instead of asked for again.
|
|
561
|
+
var smoothTeleports = false;
|
|
562
|
+
|
|
79
563
|
|
|
80
564
|
// ---------- markdown helpers ----------
|
|
81
565
|
function extractReplyText(node) {
|
|
@@ -685,16 +1169,26 @@ window.__ModuleLoader__.load({
|
|
|
685
1169
|
})();
|
|
686
1170
|
|
|
687
1171
|
// ---------- component ----------
|
|
1172
|
+
// The bail-out lives in this OUTER component, which deliberately calls NO hooks:
|
|
1173
|
+
// a slot occupant can be rendered once without session props and again with them
|
|
1174
|
+
// (a remount, a store that binds late), and a guard that returns before/after a
|
|
1175
|
+
// different number of hooks makes React abort the whole panel with
|
|
1176
|
+
// "Rendered more hooks than during the previous render" (#310). The hooks all
|
|
1177
|
+
// live in the body below, which is only ever mounted with complete props.
|
|
688
1178
|
function OutlinePanel(props) {
|
|
1179
|
+
if (!props.useChat) {
|
|
1180
|
+
console.warn("[dsh-quick-toc] useChat prop missing (requires DSH >= 0.1.5-rc.1)");
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
return react_jsx_runtime.jsx(OutlinePanelBody, props);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function OutlinePanelBody(props) {
|
|
689
1187
|
// Recent DSH (0.1.5-rc.1): the conversation moved out of the session snapshot —
|
|
690
1188
|
// it is now the session-scope `chat` hook (ChatSnapshot: order + nodes
|
|
691
1189
|
// map + legacy projection), contributed by dsh-client-ui-chat. Same node
|
|
692
1190
|
// shape as before (kind user/assistant-step, location.turn, data.blocks).
|
|
693
1191
|
var useChat = props.useChat;
|
|
694
|
-
if (!useChat) {
|
|
695
|
-
console.warn("[dsh-quick-toc] useChat prop missing (requires DSH >= 0.1.5-rc.1)");
|
|
696
|
-
return null;
|
|
697
|
-
}
|
|
698
1192
|
var order = useChat(function (s) { return s.order; });
|
|
699
1193
|
var nodes = useChat(function (s) { return s.nodes; });
|
|
700
1194
|
|
|
@@ -792,43 +1286,17 @@ window.__ModuleLoader__.load({
|
|
|
792
1286
|
};
|
|
793
1287
|
}, []);
|
|
794
1288
|
|
|
795
|
-
// dock side: 'left' | 'right' (
|
|
796
|
-
//
|
|
797
|
-
var
|
|
798
|
-
var OLD_DOCK_KEYS = ["dsh-contents.dock.v2", "dsh-dagang.dock.v2"];
|
|
799
|
-
var _s3 = react.useState(function () {
|
|
800
|
-
try {
|
|
801
|
-
var d = localStorage.getItem(DOCK_KEY);
|
|
802
|
-
if (d === "left" || d === "right") return d;
|
|
803
|
-
for (var oi = 0; oi < OLD_DOCK_KEYS.length; oi++) {
|
|
804
|
-
var old = localStorage.getItem(OLD_DOCK_KEYS[oi]);
|
|
805
|
-
if (old === "left" || old === "right") { localStorage.setItem(DOCK_KEY, old); return old; }
|
|
806
|
-
}
|
|
807
|
-
} catch (e) {}
|
|
808
|
-
return "left";
|
|
809
|
-
});
|
|
1289
|
+
// dock side: 'left' | 'right' (the store owns persistence, legacy-key migration
|
|
1290
|
+
// and the default; the panel keeps a live copy so drags stay instant)
|
|
1291
|
+
var _s3 = react.useState(function () { return prefsStore.get().dock; });
|
|
810
1292
|
var dock = _s3[0];
|
|
811
1293
|
var setDock = _s3[1];
|
|
812
1294
|
var toggleDock = function () {
|
|
813
|
-
|
|
814
|
-
setDock(next);
|
|
815
|
-
try { localStorage.setItem(DOCK_KEY, next); } catch (e) {}
|
|
1295
|
+
setDock(dock === "right" ? "left" : "right");
|
|
816
1296
|
};
|
|
817
1297
|
|
|
818
|
-
// vertical drag offset (
|
|
819
|
-
var
|
|
820
|
-
var OLD_PANEL_Y_KEYS = ["dsh-contents.panelY.v1", "dsh-dagang.panelY.v1"];
|
|
821
|
-
var _s4 = react.useState(function () {
|
|
822
|
-
try {
|
|
823
|
-
var v = localStorage.getItem(PANEL_Y_KEY);
|
|
824
|
-
if (v !== null && isFinite(Number(v))) return Number(v);
|
|
825
|
-
for (var oi = 0; oi < OLD_PANEL_Y_KEYS.length; oi++) {
|
|
826
|
-
var old = localStorage.getItem(OLD_PANEL_Y_KEYS[oi]);
|
|
827
|
-
if (old !== null && isFinite(Number(old))) { localStorage.setItem(PANEL_Y_KEY, old); return Number(old); }
|
|
828
|
-
}
|
|
829
|
-
} catch (e) {}
|
|
830
|
-
return 0;
|
|
831
|
-
});
|
|
1298
|
+
// vertical drag offset (the store owns the key and the legacy-key migration)
|
|
1299
|
+
var _s4 = react.useState(function () { return prefsStore.get().y; });
|
|
832
1300
|
var panelY = _s4[0];
|
|
833
1301
|
var setPanelY = _s4[1];
|
|
834
1302
|
var handleRef = react.useRef(null); // panel top drag bar
|
|
@@ -851,47 +1319,20 @@ window.__ModuleLoader__.load({
|
|
|
851
1319
|
var hovered = _s6[0];
|
|
852
1320
|
var setHovered = _s6[1];
|
|
853
1321
|
|
|
854
|
-
// panel size (
|
|
855
|
-
var
|
|
856
|
-
var PANEL_H_KEY = "dsh-quick-toc.panelH.v1";
|
|
857
|
-
var _s7 = react.useState(function () {
|
|
858
|
-
try { var w = Number(localStorage.getItem(PANEL_W_KEY)); if (isFinite(w) && w >= 180) return w; } catch (e) {}
|
|
859
|
-
return PANEL_WIDTH;
|
|
860
|
-
});
|
|
1322
|
+
// panel size (the store owns the keys; width 0 = the panel default, height 0 = auto)
|
|
1323
|
+
var _s7 = react.useState(function () { return prefsStore.get().w || PANEL_WIDTH; });
|
|
861
1324
|
var panelW = _s7[0];
|
|
862
1325
|
var setPanelW = _s7[1];
|
|
863
|
-
var _s8 = react.useState(function () {
|
|
864
|
-
try { var h = Number(localStorage.getItem(PANEL_H_KEY)); if (isFinite(h) && h >= 160) return h; } catch (e) {}
|
|
865
|
-
return 0;
|
|
866
|
-
});
|
|
1326
|
+
var _s8 = react.useState(function () { return prefsStore.get().h; });
|
|
867
1327
|
var panelH = _s8[0];
|
|
868
1328
|
var setPanelH = _s8[1];
|
|
869
1329
|
|
|
870
|
-
// ---- heading level filter (
|
|
1330
|
+
// ---- heading level filter (the store owns the key and the legacy migration) ----
|
|
871
1331
|
// An arbitrary SET of levels, not a "show up to N" prefix: every chip is an
|
|
872
1332
|
// independent on/off switch, so H1 + H3 without H2 is a valid view. Turning
|
|
873
1333
|
// the last remaining level off restores all six (the panel is never empty).
|
|
874
|
-
var
|
|
875
|
-
var
|
|
876
|
-
var ALL_LEVELS = [1, 2, 3, 4, 5, 6];
|
|
877
|
-
var _sLv = react.useState(function () {
|
|
878
|
-
try {
|
|
879
|
-
var raw = localStorage.getItem(LEVELS_KEY);
|
|
880
|
-
if (raw !== null) {
|
|
881
|
-
var arr = JSON.parse(raw);
|
|
882
|
-
if (Array.isArray(arr)) {
|
|
883
|
-
var kept = ALL_LEVELS.filter(function (lv) { return arr.indexOf(lv) !== -1; });
|
|
884
|
-
if (kept.length > 0) return kept;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
// migrate the "show up to N" value written by earlier builds
|
|
888
|
-
var v = Number(localStorage.getItem(OLD_MAX_LEVEL_KEY));
|
|
889
|
-
if (isFinite(v) && v >= 1 && v <= 6) {
|
|
890
|
-
return ALL_LEVELS.filter(function (lv) { return lv <= v; });
|
|
891
|
-
}
|
|
892
|
-
} catch (e) {}
|
|
893
|
-
return ALL_LEVELS.slice();
|
|
894
|
-
});
|
|
1334
|
+
var ALL_LEVELS = LEVELS_ALL;
|
|
1335
|
+
var _sLv = react.useState(function () { return prefsStore.get().levels.slice(); });
|
|
895
1336
|
var levels = _sLv[0];
|
|
896
1337
|
var setLevels = _sLv[1];
|
|
897
1338
|
var levelSet = react.useMemo(function () {
|
|
@@ -908,7 +1349,6 @@ window.__ModuleLoader__.load({
|
|
|
908
1349
|
next = ALL_LEVELS.filter(function (x) { return x === lv || levelSet[x]; });
|
|
909
1350
|
}
|
|
910
1351
|
setLevels(next);
|
|
911
|
-
try { localStorage.setItem(LEVELS_KEY, JSON.stringify(next)); } catch (e) {}
|
|
912
1352
|
};
|
|
913
1353
|
|
|
914
1354
|
// ---- heading-level picker popup (transient): opens from the round button
|
|
@@ -1033,6 +1473,9 @@ window.__ModuleLoader__.load({
|
|
|
1033
1473
|
var hoverTimerRef = react.useRef(null);
|
|
1034
1474
|
var hoverCloseTimerRef = react.useRef(null);
|
|
1035
1475
|
var hoverStart = function (info, el) {
|
|
1476
|
+
// the hover card is a preference: off means it never opens (the outline rows
|
|
1477
|
+
// themselves keep their hover tint)
|
|
1478
|
+
if (!prefsStore.get().hover) return;
|
|
1036
1479
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
1037
1480
|
hoverTimerRef.current = setTimeout(function () {
|
|
1038
1481
|
var r = el && el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
@@ -1139,18 +1582,79 @@ window.__ModuleLoader__.load({
|
|
|
1139
1582
|
// ---- fuzzy matching: an independent switch beside the scope pill --------
|
|
1140
1583
|
// Off (default) = the folded CONTIGUOUS match. On = subsequence matching, so
|
|
1141
1584
|
// keywords tolerate text wedged in between ("模糊匹配" also finds "模糊的匹配").
|
|
1142
|
-
var
|
|
1143
|
-
var _sFz = react.useState(function () {
|
|
1144
|
-
try { return localStorage.getItem(FUZZY_KEY) === "1"; } catch (e) { return false; }
|
|
1145
|
-
});
|
|
1585
|
+
var _sFz = react.useState(function () { return prefsStore.get().fuzzy; });
|
|
1146
1586
|
var fuzzy = _sFz[0];
|
|
1147
1587
|
var setFuzzy = _sFz[1];
|
|
1148
1588
|
var toggleFuzzy = function () {
|
|
1149
|
-
|
|
1150
|
-
setFuzzy(next);
|
|
1589
|
+
setFuzzy(!fuzzy);
|
|
1151
1590
|
setMatchIdx(0);
|
|
1152
|
-
try { localStorage.setItem(FUZZY_KEY, next ? "1" : "0"); } catch (e) {}
|
|
1153
1591
|
};
|
|
1592
|
+
|
|
1593
|
+
// ---- preference plumbing -------------------------------------------------
|
|
1594
|
+
// The store is the shared source (the native Settings section writes into it);
|
|
1595
|
+
// the states above are the panel's LIVE copy, so a drag or a toggle never waits
|
|
1596
|
+
// for a round-trip. `lastSeen` records every value the panel has already agreed
|
|
1597
|
+
// on, which is what separates "someone else changed this" (adopt it) from "the
|
|
1598
|
+
// panel itself just changed it" (leave the live value alone — otherwise a stale
|
|
1599
|
+
// store value would snap a half-finished drag back).
|
|
1600
|
+
//
|
|
1601
|
+
// Both effects MUST agree on the exact shape they compare, so every entry is kept
|
|
1602
|
+
// in the panel's own form rather than the stored one: `levels` as the joined key
|
|
1603
|
+
// (never the raw array) and `w` as the EFFECTIVE pixel width (a stored 0 only
|
|
1604
|
+
// means "the panel default"). Recording the raw stored value instead makes the
|
|
1605
|
+
// two comparisons disagree forever — the panel then republishes its default on
|
|
1606
|
+
// every pass and stomps a fresh change with the value it had just read back.
|
|
1607
|
+
var prefs = usePrefs();
|
|
1608
|
+
// Resolve the language for this render FIRST: the group builder below bakes the
|
|
1609
|
+
// localised turn stamps (昨天 versus a numeric date) into its memo, and `langKey`
|
|
1610
|
+
// is what tells that memo a language switch has to rebuild it.
|
|
1611
|
+
var langKey = setLanguage(prefs.lang, hostLanguage());
|
|
1612
|
+
// What the panel last handed to the store. A store value that comes back equal to
|
|
1613
|
+
// this is our own echo, not news — and the panel may already have moved on: it
|
|
1614
|
+
// restores all six levels the moment the last one is switched off, long before
|
|
1615
|
+
// that restore is written out, and the echo of the previous write would otherwise
|
|
1616
|
+
// undo it. So only a value that differs from BOTH the live state and this record is
|
|
1617
|
+
// an external change worth adopting. Size and position are not adopted at all:
|
|
1618
|
+
// this panel is their only writer, so a pull can never bring news, only a stale
|
|
1619
|
+
// copy — which is exactly what made a drag spring back to its previous size.
|
|
1620
|
+
var sent = react.useRef({
|
|
1621
|
+
dock: prefs.dock, y: prefs.y, w: prefs.w || PANEL_WIDTH, h: prefs.h,
|
|
1622
|
+
levels: prefs.levels.join(","), fuzzy: prefs.fuzzy
|
|
1623
|
+
});
|
|
1624
|
+
react.useEffect(function () {
|
|
1625
|
+
var localLevels = levels.join(",");
|
|
1626
|
+
var storeLevels = prefs.levels.join(",");
|
|
1627
|
+
if (prefs.dock !== dock && prefs.dock !== sent.current.dock) setDock(prefs.dock);
|
|
1628
|
+
if (storeLevels !== localLevels && storeLevels !== sent.current.levels) setLevels(prefs.levels.slice());
|
|
1629
|
+
if (prefs.fuzzy !== fuzzy && prefs.fuzzy !== sent.current.fuzzy) setFuzzy(prefs.fuzzy);
|
|
1630
|
+
}, [prefs]);
|
|
1631
|
+
// ...and publish the panel's own changes back, debounced: a drag fires on every
|
|
1632
|
+
// pointer move and the Settings section does not need that resolution.
|
|
1633
|
+
react.useEffect(function () {
|
|
1634
|
+
var seen = sent.current;
|
|
1635
|
+
var nextLevels = levels.join(",");
|
|
1636
|
+
var patch = {};
|
|
1637
|
+
if (dock !== seen.dock) patch.dock = dock;
|
|
1638
|
+
if (panelY !== seen.y) patch.y = panelY;
|
|
1639
|
+
if (panelW !== seen.w) patch.w = panelW;
|
|
1640
|
+
if (panelH !== seen.h) patch.h = panelH;
|
|
1641
|
+
if (nextLevels !== seen.levels) patch.levels = levels.slice();
|
|
1642
|
+
if (fuzzy !== seen.fuzzy) patch.fuzzy = fuzzy;
|
|
1643
|
+
if (!Object.keys(patch).length) return;
|
|
1644
|
+
// agree on the panel's form of each published value (see the note above), so the
|
|
1645
|
+
// next pass — including the one the store's own notification triggers — finds
|
|
1646
|
+
// nothing left to publish and the debounced write cannot bounce back
|
|
1647
|
+
var has = function (key) { return Object.prototype.hasOwnProperty.call(patch, key); };
|
|
1648
|
+
if (has("dock")) seen.dock = patch.dock;
|
|
1649
|
+
if (has("y")) seen.y = patch.y;
|
|
1650
|
+
if (has("w")) seen.w = patch.w;
|
|
1651
|
+
if (has("h")) seen.h = patch.h;
|
|
1652
|
+
if (has("levels")) seen.levels = nextLevels;
|
|
1653
|
+
if (has("fuzzy")) seen.fuzzy = patch.fuzzy;
|
|
1654
|
+
var timer = setTimeout(function () { prefsStore.set(patch); }, 400);
|
|
1655
|
+
return function () { clearTimeout(timer); };
|
|
1656
|
+
}, [dock, panelY, panelW, panelH, levels, fuzzy]);
|
|
1657
|
+
|
|
1154
1658
|
var openSearch = function () {
|
|
1155
1659
|
setSearchOpen(true);
|
|
1156
1660
|
setSearchAnim("enter");
|
|
@@ -1322,23 +1826,39 @@ window.__ModuleLoader__.load({
|
|
|
1322
1826
|
}
|
|
1323
1827
|
};
|
|
1324
1828
|
|
|
1829
|
+
// ---- panel scale (prefs.zoom) ------------------------------------------
|
|
1830
|
+
// The preference magnifies what is INSIDE the panel — text, icons, buttons and
|
|
1831
|
+
// their spacing — and never the panel's own box: the reader keeps full control
|
|
1832
|
+
// of the size by dragging. A CSS zoom on the panel would scale the box as well,
|
|
1833
|
+
// so every length that defines the box is divided by the factor and zoom
|
|
1834
|
+
// multiplies it straight back: the box stays exactly the size that was dragged
|
|
1835
|
+
// while the content inside is magnified (and simply shows less of itself).
|
|
1836
|
+
// Measured on this Chromium: zoom scales the element's own box, its insets, its
|
|
1837
|
+
// clip-path and viewport units, and getBoundingClientRect reports the scaled
|
|
1838
|
+
// (visual) values.
|
|
1839
|
+
var zoom = prefs.zoom > 0 ? prefs.zoom : 1;
|
|
1840
|
+
// a length that must keep its VISUAL value under zoom (zoom multiplies it back)
|
|
1841
|
+
var zLen = function (v) {
|
|
1842
|
+
if (zoom === 1) return typeof v === "number" ? v + "px" : v;
|
|
1843
|
+
return typeof v === "number" ? (v / zoom) + "px" : "calc(" + v + " / " + zoom + ")";
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1325
1846
|
// ---- resize drags (right edge = width, bottom edge = height) ----
|
|
1847
|
+
// No upper bound and only a keep-it-usable floor: the reader decides how small
|
|
1848
|
+
// the panel gets. The drags only move the live state; the preference effect
|
|
1849
|
+
// persists the settled value (debounced), so there is exactly one writer.
|
|
1326
1850
|
var onResizeWDown = function (e) {
|
|
1327
1851
|
e.preventDefault();
|
|
1328
1852
|
e.stopPropagation();
|
|
1329
1853
|
if (e.button !== 0) return;
|
|
1330
1854
|
var startX = e.clientX;
|
|
1331
1855
|
var startW = panelW;
|
|
1332
|
-
var lastW = panelW;
|
|
1333
1856
|
var move = function (ev) {
|
|
1334
|
-
|
|
1335
|
-
lastW = next;
|
|
1336
|
-
setPanelW(next);
|
|
1857
|
+
setPanelW(Math.max(120, startW + (ev.clientX - startX)));
|
|
1337
1858
|
};
|
|
1338
1859
|
var up = function () {
|
|
1339
1860
|
window.removeEventListener("pointermove", move);
|
|
1340
1861
|
window.removeEventListener("pointerup", up);
|
|
1341
|
-
try { localStorage.setItem(PANEL_W_KEY, String(lastW)); } catch (e2) {}
|
|
1342
1862
|
};
|
|
1343
1863
|
window.addEventListener("pointermove", move);
|
|
1344
1864
|
window.addEventListener("pointerup", up);
|
|
@@ -1349,16 +1869,12 @@ window.__ModuleLoader__.load({
|
|
|
1349
1869
|
if (e.button !== 0) return;
|
|
1350
1870
|
var startY = e.clientY;
|
|
1351
1871
|
var startH = panelH > 0 ? panelH : 400;
|
|
1352
|
-
var lastH = startH;
|
|
1353
1872
|
var move = function (ev) {
|
|
1354
|
-
|
|
1355
|
-
lastH = next;
|
|
1356
|
-
setPanelH(next);
|
|
1873
|
+
setPanelH(Math.max(60, startH + (ev.clientY - startY)));
|
|
1357
1874
|
};
|
|
1358
1875
|
var up = function () {
|
|
1359
1876
|
window.removeEventListener("pointermove", move);
|
|
1360
1877
|
window.removeEventListener("pointerup", up);
|
|
1361
|
-
try { localStorage.setItem(PANEL_H_KEY, String(lastH)); } catch (e2) {}
|
|
1362
1878
|
};
|
|
1363
1879
|
window.addEventListener("pointermove", move);
|
|
1364
1880
|
window.addEventListener("pointerup", up);
|
|
@@ -1371,69 +1887,26 @@ window.__ModuleLoader__.load({
|
|
|
1371
1887
|
var startY = e.clientY;
|
|
1372
1888
|
var startW = panelW;
|
|
1373
1889
|
var startH = panelH > 0 ? panelH : 400;
|
|
1374
|
-
var lastW = startW;
|
|
1375
|
-
var lastH = startH;
|
|
1376
1890
|
var move = function (ev) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
lastW = w;
|
|
1380
|
-
lastH = h;
|
|
1381
|
-
setPanelW(w);
|
|
1382
|
-
setPanelH(h);
|
|
1891
|
+
setPanelW(Math.max(120, startW + (ev.clientX - startX)));
|
|
1892
|
+
setPanelH(Math.max(60, startH + (ev.clientY - startY)));
|
|
1383
1893
|
};
|
|
1384
1894
|
var up = function () {
|
|
1385
1895
|
window.removeEventListener("pointermove", move);
|
|
1386
1896
|
window.removeEventListener("pointerup", up);
|
|
1387
|
-
try {
|
|
1388
|
-
localStorage.setItem(PANEL_W_KEY, String(lastW));
|
|
1389
|
-
localStorage.setItem(PANEL_H_KEY, String(lastH));
|
|
1390
|
-
} catch (e2) {}
|
|
1391
1897
|
};
|
|
1392
1898
|
window.addEventListener("pointermove", move);
|
|
1393
1899
|
window.addEventListener("pointerup", up);
|
|
1394
1900
|
};
|
|
1395
1901
|
|
|
1396
|
-
// ---- node timestamp ->
|
|
1397
|
-
|
|
1398
|
-
if (t === undefined || t === null) return null;
|
|
1399
|
-
if (typeof t === "string") {
|
|
1400
|
-
var d0 = new Date(t);
|
|
1401
|
-
return isNaN(d0.getTime()) ? null : d0;
|
|
1402
|
-
}
|
|
1403
|
-
var n = Number(t);
|
|
1404
|
-
if (!isFinite(n) || n <= 0) return null;
|
|
1405
|
-
if (n < 1e12) n = n * 1000; // epoch seconds -> ms
|
|
1406
|
-
var d = new Date(n);
|
|
1407
|
-
return isNaN(d.getTime()) ? null : d;
|
|
1408
|
-
};
|
|
1409
|
-
var pad2 = function (n) { return ("0" + n).slice(-2); };
|
|
1410
|
-
// ---- node timestamp -> HH:MM ----
|
|
1411
|
-
var fmtTime = function (t) {
|
|
1412
|
-
var d = toDate(t);
|
|
1413
|
-
return d ? pad2(d.getHours()) + ":" + pad2(d.getMinutes()) : "";
|
|
1414
|
-
};
|
|
1415
|
-
|
|
1416
|
-
// ---- turn stamp: the clock alone is ambiguous in a session that spans days,
|
|
1417
|
-
// so today stays "HH:MM", yesterday and the day before get a word, and anything
|
|
1418
|
-
// older gets its date as YY-MM-DD (weekday-free, sortable, short).
|
|
1419
|
-
var startOfDay = function (d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); };
|
|
1420
|
-
var fmtStamp = function (t) {
|
|
1421
|
-
var d = toDate(t);
|
|
1422
|
-
if (!d) return "";
|
|
1423
|
-
var clock = pad2(d.getHours()) + ":" + pad2(d.getMinutes());
|
|
1424
|
-
var days = Math.round((startOfDay(new Date()) - startOfDay(d)) / 86400000);
|
|
1425
|
-
if (days <= 0) return clock; // today (a future skew also reads as today)
|
|
1426
|
-
if (days === 1) return T("time.yesterday") + " " + clock;
|
|
1427
|
-
if (days === 2) return T("time.beforeYesterday") + " " + clock;
|
|
1428
|
-
return pad2(d.getFullYear() % 100) + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) + " " + clock;
|
|
1429
|
-
};
|
|
1430
|
-
|
|
1902
|
+
// ---- node timestamp -> a readable stamp (helpers live at module scope so the
|
|
1903
|
+
// language-sensitive formatting has exactly one home) ----
|
|
1431
1904
|
// ---- best-effort node time across common field names ----
|
|
1432
1905
|
var getNodeTime = function (node) {
|
|
1433
1906
|
var d = node && node.data;
|
|
1434
1907
|
if (!d) return "";
|
|
1435
1908
|
var t = d.time !== undefined ? d.time : (d.createdAt !== undefined ? d.createdAt : d.timestamp);
|
|
1436
|
-
return
|
|
1909
|
+
return fmtStampOf(t);
|
|
1437
1910
|
};
|
|
1438
1911
|
|
|
1439
1912
|
// ---- group headings by conversation turn (node.location.turn) ----
|
|
@@ -1559,7 +2032,7 @@ window.__ModuleLoader__.load({
|
|
|
1559
2032
|
}
|
|
1560
2033
|
while (li < ready.length) { merged.push(ready[li]); li++; }
|
|
1561
2034
|
return merged;
|
|
1562
|
-
}, [order, nodes, outlineTurns]);
|
|
2035
|
+
}, [order, nodes, outlineTurns, langKey]);
|
|
1563
2036
|
// first time content appears, scroll the list to the bottom (newest).
|
|
1564
2037
|
// NOTE: must stay BELOW the `groups` memo — a deps array is evaluated
|
|
1565
2038
|
// during render, so reading `groups` before that `var` is assigned would
|
|
@@ -1578,21 +2051,38 @@ window.__ModuleLoader__.load({
|
|
|
1578
2051
|
}, [groups.length, query, visibleCount, panelW, panelH, open]);
|
|
1579
2052
|
// Mount diagnostic: the host capabilities this panel actually reached
|
|
1580
2053
|
// (whole-log turn index + jump loader). SILENT by default — a normal console
|
|
1581
|
-
// should stay clean — and switched on
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
2054
|
+
// should stay clean — and switched on from the plugin-configuration card
|
|
2055
|
+
// ("print the mount diagnostic"), which persists the flag in the host settings
|
|
2056
|
+
// document. Kept because it is the fastest way to tell "the host has no
|
|
1584
2057
|
// turnOutline projection" apart from "the jump-loader bridge is broken".
|
|
1585
2058
|
var mountLogRef = react.useRef(false);
|
|
1586
2059
|
react.useEffect(function () {
|
|
1587
2060
|
if (mountLogRef.current) return;
|
|
2061
|
+
if (!prefsStore.get().debug) return; // the host has not answered yet, or it is off
|
|
1588
2062
|
mountLogRef.current = true;
|
|
1589
|
-
var debug = false;
|
|
1590
|
-
try { debug = localStorage.getItem("dsh-quick-toc.debug") === "1"; } catch (e) { debug = false; }
|
|
1591
|
-
if (!debug) return;
|
|
1592
2063
|
console.log("[dsh-quick-toc] panel mounted · turnOutline=" +
|
|
1593
2064
|
(outlineTurns ? outlineTurns.length + " turns" : "unavailable") +
|
|
1594
|
-
" · jumpLoader=" + jumpLoaderState()
|
|
1595
|
-
|
|
2065
|
+
" · jumpLoader=" + jumpLoaderState() +
|
|
2066
|
+
" · lang=" + activeLang + (langPref === "auto" ? " (auto)" : "") +
|
|
2067
|
+
" · prefs=" + JSON.stringify(prefsStore.get()));
|
|
2068
|
+
// `prefs` too: the host snapshot answers after mount, so a switch turned on in
|
|
2069
|
+
// the card prints on the spot instead of asking for a reload
|
|
2070
|
+
}, [outlineTurns, prefs]);
|
|
2071
|
+
|
|
2072
|
+
// Locator fallback for a turn the jump loader just paged in: once the paged
|
|
2073
|
+
// window covers the turn, OUR OWN rebuilt outline has a loaded group for it,
|
|
2074
|
+
// which carries that turn's node keys — that path does not depend on the host
|
|
2075
|
+
// keeping a `data-chat-turn` attribute on the row.
|
|
2076
|
+
//
|
|
2077
|
+
// NOTE: this hook pair lives HERE, with the other hooks, and NOT next to the
|
|
2078
|
+
// function that reads it further down. Hooks must run on EVERY render: the
|
|
2079
|
+
// panel bails out early while the conversation is still empty (no groups), so a
|
|
2080
|
+
// hook declared after that return is skipped on the empty render and added back
|
|
2081
|
+
// on the next one — "Rendered more hooks than during the previous render"
|
|
2082
|
+
// (React #310), which killed the panel exactly when a session went from empty
|
|
2083
|
+
// to loaded.
|
|
2084
|
+
var groupsRef = react.useRef([]);
|
|
2085
|
+
react.useEffect(function () { groupsRef.current = groups; }, [groups]);
|
|
1596
2086
|
|
|
1597
2087
|
var groupTrees = react.useMemo(function () {
|
|
1598
2088
|
return groups.map(function (g) {
|
|
@@ -1751,6 +2241,11 @@ window.__ModuleLoader__.load({
|
|
|
1751
2241
|
var followTimer = null;
|
|
1752
2242
|
var lastRun = 0;
|
|
1753
2243
|
var update = function (force) {
|
|
2244
|
+
// a jump glide carries the conversation past many turns; following each
|
|
2245
|
+
// transient position would make the rail sprint through the whole
|
|
2246
|
+
// outline (wild jumps on rapid consecutive clicks), so hold still while
|
|
2247
|
+
// one is in flight — the poll syncs the rail to the landing afterwards
|
|
2248
|
+
if (glide) return;
|
|
1754
2249
|
// scroll frames are hot and a document-level capture listener sees every
|
|
1755
2250
|
// scroll in the app; the rail only changes when a different turn crosses
|
|
1756
2251
|
// the viewport, so cap the row measurement at ~16/s
|
|
@@ -1893,6 +2388,20 @@ window.__ModuleLoader__.load({
|
|
|
1893
2388
|
setHint(canLoadOlder() ? "more" : "");
|
|
1894
2389
|
}, [query, matches.length]);
|
|
1895
2390
|
|
|
2391
|
+
// keep the outline filled: after opening the panel or dragging it taller,
|
|
2392
|
+
// the latest page may be shorter than the list viewport, leaving dead space
|
|
2393
|
+
// below the newest group. Grow the window until the content fills the height
|
|
2394
|
+
// (or every group is already shown). Outline mode only — the search list
|
|
2395
|
+
// grows with its matches.
|
|
2396
|
+
react.useEffect(function () {
|
|
2397
|
+
if (query.trim() !== "") return;
|
|
2398
|
+
var el = listRef.current;
|
|
2399
|
+
if (!el || el.clientHeight === 0) return;
|
|
2400
|
+
if (el.scrollHeight - el.clientHeight > 4) return; // content overflows: nothing blank below
|
|
2401
|
+
if (visibleCount >= groups.length) return;
|
|
2402
|
+
setVisibleCount(Math.min(groups.length, visibleCount + PAGE_SIZE));
|
|
2403
|
+
}, [groups.length, visibleCount, panelW, panelH, open, query]);
|
|
2404
|
+
|
|
1896
2405
|
// keep the current row in view while stepping with Enter
|
|
1897
2406
|
react.useEffect(function () {
|
|
1898
2407
|
if (!query.trim() || activeResultRow < 0) return;
|
|
@@ -1906,10 +2415,52 @@ window.__ModuleLoader__.load({
|
|
|
1906
2415
|
else if (er.bottom > lr2.bottom - 2) el.scrollTop += er.bottom - lr2.bottom + 2;
|
|
1907
2416
|
}, [activeResultRow, query]);
|
|
1908
2417
|
|
|
2418
|
+
// ---- crowded header: squeeze the middle, then scroll sideways -----------
|
|
2419
|
+
// Four fixed-size controls share one header row. While there is room, the
|
|
2420
|
+
// flexible spacer between the two pairs takes up the slack; once the controls
|
|
2421
|
+
// would touch, the spacer is already at zero and the row becomes a sideways
|
|
2422
|
+
// scroll strip — the wheel scrolls it, and the grab bar above fades out because
|
|
2423
|
+
// there is no slack left to signal. Declared BEFORE the "nothing to show" return
|
|
2424
|
+
// below, so the hook count never depends on the data (the React #310 lesson).
|
|
2425
|
+
var headerRef = react.useRef(null);
|
|
2426
|
+
var _sCrowd = react.useState(false);
|
|
2427
|
+
var crowded = _sCrowd[0];
|
|
2428
|
+
var setCrowded = _sCrowd[1];
|
|
2429
|
+
// A scale change moves the box's LOCAL lengths while `zoom` itself cannot
|
|
2430
|
+
// animate: that single render skips the `left` transition instead of sliding.
|
|
2431
|
+
var prevZoom = react.useRef(zoom);
|
|
2432
|
+
var zoomJustChanged = prevZoom.current !== zoom;
|
|
2433
|
+
react.useEffect(function () { prevZoom.current = zoom; }, [zoom]);
|
|
2434
|
+
react.useEffect(function () {
|
|
2435
|
+
var el = headerRef.current;
|
|
2436
|
+
if (!el) return;
|
|
2437
|
+
var tight = el.scrollWidth > el.clientWidth + 1;
|
|
2438
|
+
setCrowded(function (prev) { return prev === tight ? prev : tight; });
|
|
2439
|
+
}, [open, panelW, zoom, dock, levelsOpen, searchOpen, groups.length]);
|
|
2440
|
+
react.useEffect(function () {
|
|
2441
|
+
var el = headerRef.current;
|
|
2442
|
+
if (!el) return;
|
|
2443
|
+
// `crowded` is captured here and the listener is re-attached whenever it
|
|
2444
|
+
// changes, so the hot path (every wheel event) reads no layout at all —
|
|
2445
|
+
// measuring scrollWidth per event would force a synchronous layout.
|
|
2446
|
+
if (!crowded) return;
|
|
2447
|
+
var onWheel = function (e) {
|
|
2448
|
+
var d = e.deltaY !== 0 ? e.deltaY : e.deltaX;
|
|
2449
|
+
if (!d) return;
|
|
2450
|
+
el.scrollLeft += d;
|
|
2451
|
+
e.preventDefault(); // the transcript behind must not scroll instead
|
|
2452
|
+
e.stopPropagation();
|
|
2453
|
+
};
|
|
2454
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
2455
|
+
return function () { el.removeEventListener("wheel", onWheel); };
|
|
2456
|
+
}, [open, crowded, groups.length]);
|
|
2457
|
+
|
|
1909
2458
|
// ---- nothing to show without headings ----
|
|
1910
2459
|
if (groups.length === 0) return null;
|
|
1911
2460
|
|
|
1912
2461
|
// ---- geometry: always position by `left` (px) so dock-switch & slide share one animated transition
|
|
2462
|
+
// (The scale preference leaves all of this alone: it acts on the panel's inner
|
|
2463
|
+
// box, so the panel itself keeps the width and height the reader dragged.)
|
|
1913
2464
|
var dockRight = dock === "right";
|
|
1914
2465
|
var panelLeft;
|
|
1915
2466
|
if (dockRight) {
|
|
@@ -1937,36 +2488,45 @@ window.__ModuleLoader__.load({
|
|
|
1937
2488
|
if (e.button !== 0) return;
|
|
1938
2489
|
var startY = e.clientY;
|
|
1939
2490
|
var origY = panelY;
|
|
1940
|
-
var lastY = panelY;
|
|
1941
2491
|
var move = function (ev) {
|
|
1942
2492
|
var next = origY + (ev.clientY - startY);
|
|
1943
2493
|
next = Math.max(-(viewport ? viewport.top : 0), Math.min(window.innerHeight - 90, next));
|
|
1944
|
-
lastY = next;
|
|
1945
2494
|
setPanelY(next);
|
|
1946
2495
|
};
|
|
1947
2496
|
var up = function () {
|
|
1948
2497
|
window.removeEventListener("pointermove", move);
|
|
1949
2498
|
window.removeEventListener("pointerup", up);
|
|
1950
|
-
// persist the DRAGGED value: `panelY` here is the value captured when
|
|
1951
|
-
// the drag started (the state setter has not re-rendered this closure)
|
|
1952
|
-
try { localStorage.setItem(PANEL_Y_KEY, String(lastY)); } catch (e2) {}
|
|
1953
2499
|
};
|
|
1954
2500
|
window.addEventListener("pointermove", move);
|
|
1955
2501
|
window.addEventListener("pointerup", up);
|
|
1956
2502
|
};
|
|
1957
2503
|
var handleBright = function (on) {
|
|
1958
|
-
|
|
2504
|
+
// while the header is crowded the grab hint stays hidden even on hover
|
|
2505
|
+
if (handleRef.current) handleRef.current.style.opacity = crowded ? "0" : (on ? "1" : "0.35");
|
|
1959
2506
|
};
|
|
1960
2507
|
|
|
1961
2508
|
// ---- jump: glide to the exact heading element ---------------------------
|
|
1962
|
-
//
|
|
1963
|
-
//
|
|
1964
|
-
//
|
|
1965
|
-
//
|
|
1966
|
-
//
|
|
1967
|
-
//
|
|
1968
|
-
//
|
|
1969
|
-
//
|
|
2509
|
+
// The glide is the browser's own smooth scrolling. It runs off the wall
|
|
2510
|
+
// clock, not off a frame count, so it takes the same time on a 60Hz and on
|
|
2511
|
+
// a 240Hz panel, and it coasts to a stop instead of stopping dead. Three
|
|
2512
|
+
// guards, each one learned from a real failure:
|
|
2513
|
+
// * DSH holds the transcript at the bottom. A jump upward has to step out
|
|
2514
|
+
// of that 25px stick zone first, or the compensation scroll that comes
|
|
2515
|
+
// with it undoes the jump.
|
|
2516
|
+
// * A long jump makes the host page history in, which moves the target
|
|
2517
|
+
// while the animation is running, and the host's own compensation
|
|
2518
|
+
// scrolls cancel the animation outright. So the landing is watched and
|
|
2519
|
+
// the call re-issued — but only on a real drift (>40px) or a real stop
|
|
2520
|
+
// (>400ms with less than a pixel of movement). A watchdog that cannot
|
|
2521
|
+
// tell a slow start from a dead scroll re-issues on every tick, and
|
|
2522
|
+
// then the jump never animates at all: this curve begins with almost no
|
|
2523
|
+
// movement by design.
|
|
2524
|
+
// * If a browser carries the call out as an instant jump (none seen; the
|
|
2525
|
+
// check is cheap) the glide is drawn frame by frame instead, over a
|
|
2526
|
+
// real-time duration for the same reason as above.
|
|
2527
|
+
// A short re-check after the landing keeps it honest while the host
|
|
2528
|
+
// finishes measuring; a newer jump retires the previous one; and the
|
|
2529
|
+
// auto-follow stays out of the way while one is in flight.
|
|
1970
2530
|
var scrollTargetOf = function (el, sp) {
|
|
1971
2531
|
var t = el.getBoundingClientRect().top - sp.getBoundingClientRect().top + sp.scrollTop - 20;
|
|
1972
2532
|
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
@@ -1974,28 +2534,94 @@ window.__ModuleLoader__.load({
|
|
|
1974
2534
|
};
|
|
1975
2535
|
var glideTo = function (el) {
|
|
1976
2536
|
var sp = el.closest ? el.closest("[data-conversation-scroll]") : null;
|
|
1977
|
-
if (!sp)
|
|
1978
|
-
|
|
1979
|
-
return;
|
|
1980
|
-
}
|
|
1981
|
-
var t = scrollTargetOf(el, sp);
|
|
2537
|
+
if (!sp) return;
|
|
2538
|
+
var t0 = scrollTargetOf(el, sp);
|
|
1982
2539
|
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
1983
|
-
if (floor - sp.scrollTop <= 25 && Math.abs(
|
|
1984
|
-
sp.scrollTop = Math.max(0, floor - 26);
|
|
2540
|
+
if (floor - sp.scrollTop <= 25 && Math.abs(t0 - sp.scrollTop) > 60) {
|
|
2541
|
+
sp.scrollTop = Math.max(0, floor - 26); // break the stick-to-bottom hold first
|
|
1985
2542
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2543
|
+
var from = sp.scrollTop;
|
|
2544
|
+
var dist = Math.abs(t0 - from);
|
|
2545
|
+
if (dist < 2) return;
|
|
2546
|
+
if (glide) glide.done = true; // a newer jump takes over
|
|
2547
|
+
var mine = { done: false };
|
|
2548
|
+
glide = mine;
|
|
2549
|
+
var aimed = t0;
|
|
2550
|
+
var drawn = false;
|
|
2551
|
+
var landed = false;
|
|
2552
|
+
|
|
2553
|
+
// landed: hand control back to the auto-follow immediately — holding the
|
|
2554
|
+
// glide until the re-checks finish would delay the "currently reading"
|
|
2555
|
+
// highlight by ~1s. The re-checks keep running in the background; they
|
|
2556
|
+
// only nudge small offsets, which cannot re-trigger the wild rail chase.
|
|
2557
|
+
var land = function () {
|
|
2558
|
+
if (landed) return;
|
|
2559
|
+
landed = true;
|
|
2560
|
+
if (glide === mine) glide = null;
|
|
2561
|
+
var ticks = 0;
|
|
2562
|
+
var iv = setInterval(function () {
|
|
2563
|
+
if (mine.done || (ticks += 1) > 5) { clearInterval(iv); return; }
|
|
2564
|
+
var next = scrollTargetOf(el, sp);
|
|
2565
|
+
if (Math.abs(next - sp.scrollTop) > 2) sp.scrollTop = next;
|
|
2566
|
+
}, 120);
|
|
2567
|
+
};
|
|
2568
|
+
|
|
2569
|
+
// fallback: draw the glide ourselves, in real time, decelerating to a stop
|
|
2570
|
+
var draw = function () {
|
|
2571
|
+
if (drawn) return;
|
|
2572
|
+
drawn = true;
|
|
2573
|
+
var start = Math.abs(sp.scrollTop - aimed) <= 2 ? from : sp.scrollTop; // undo an instant jump
|
|
2574
|
+
// paced like the browser's own glide: ~0.7s for two screens, ~1.3s across a long session
|
|
2575
|
+
var dur = Math.min(1400, Math.max(300, 190 + Math.sqrt(Math.abs(aimed - start)) * 13));
|
|
2576
|
+
var started = Date.now();
|
|
2577
|
+
sp.scrollTop = start;
|
|
2578
|
+
var frame = function () {
|
|
2579
|
+
if (mine.done) { if (glide === mine) glide = null; return; }
|
|
2580
|
+
var k = Math.min(1, (Date.now() - started) / dur);
|
|
2581
|
+
var eased = 1 - (1 - k) * (1 - k); // quick off the mark, long coast to a stop
|
|
2582
|
+
var live = scrollTargetOf(el, sp); // the target's offset RIGHT NOW
|
|
2583
|
+
sp.scrollTop = start + (live - start) * eased;
|
|
2584
|
+
if (k < 1) requestAnimationFrame(frame);
|
|
2585
|
+
else land();
|
|
2586
|
+
};
|
|
2587
|
+
requestAnimationFrame(frame);
|
|
2588
|
+
};
|
|
2589
|
+
|
|
2590
|
+
var aim = function (top) {
|
|
2591
|
+
aimed = top;
|
|
2592
|
+
if (drawn) return; // a drawn glide cannot be re-aimed through the browser
|
|
2593
|
+
if (smoothTeleports) { draw(); return; }
|
|
2594
|
+
try { sp.scrollTo({ top: top, behavior: "smooth" }); }
|
|
2595
|
+
catch (e) { sp.scrollTop = top; }
|
|
2596
|
+
};
|
|
2597
|
+
|
|
2598
|
+
aim(t0);
|
|
2599
|
+
if (!smoothTeleports && dist > (sp.clientHeight || 400) * 1.5) {
|
|
2600
|
+
// one frame is enough to tell the two apart: an animation cannot have
|
|
2601
|
+
// covered that much ground yet, whereas a jump carried out at once is
|
|
2602
|
+
// already there and has to be drawn by hand from now on.
|
|
2603
|
+
requestAnimationFrame(function () {
|
|
2604
|
+
if (mine.done || drawn) return;
|
|
2605
|
+
if (Math.abs(sp.scrollTop - t0) <= 2) { smoothTeleports = true; draw(); }
|
|
2606
|
+
});
|
|
1989
2607
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
var
|
|
1994
|
-
var
|
|
1995
|
-
|
|
1996
|
-
var
|
|
1997
|
-
|
|
1998
|
-
if (
|
|
2608
|
+
|
|
2609
|
+
var lastTop = from;
|
|
2610
|
+
var lastMove = Date.now();
|
|
2611
|
+
var stalls = 0;
|
|
2612
|
+
var watch = setInterval(function () {
|
|
2613
|
+
if (mine.done || glide !== mine || drawn) { clearInterval(watch); return; }
|
|
2614
|
+
var live = scrollTargetOf(el, sp);
|
|
2615
|
+
var top = sp.scrollTop;
|
|
2616
|
+
if (Math.abs(live - aimed) > 40) { aim(live); lastTop = top; lastMove = Date.now(); return; }
|
|
2617
|
+
if (Math.abs(top - aimed) <= 2) { clearInterval(watch); land(); return; }
|
|
2618
|
+
if (Math.abs(top - lastTop) > 1) { lastTop = top; lastMove = Date.now(); stalls = 0; return; }
|
|
2619
|
+
if (Date.now() - lastMove > 400) { // cancelled, or the call never took
|
|
2620
|
+
stalls += 1;
|
|
2621
|
+
if (stalls >= 2) draw();
|
|
2622
|
+
aim(aimed);
|
|
2623
|
+
lastMove = Date.now();
|
|
2624
|
+
}
|
|
1999
2625
|
}, 120);
|
|
2000
2626
|
};
|
|
2001
2627
|
|
|
@@ -2024,12 +2650,7 @@ window.__ModuleLoader__.load({
|
|
|
2024
2650
|
}
|
|
2025
2651
|
};
|
|
2026
2652
|
|
|
2027
|
-
//
|
|
2028
|
-
// turn, OUR OWN rebuilt outline has a loaded group for it, which carries the
|
|
2029
|
-
// anchor keys of that turn's nodes. That path does not depend on the host
|
|
2030
|
-
// keeping a `data-chat-turn` attribute on the row.
|
|
2031
|
-
var groupsRef = react.useRef([]);
|
|
2032
|
-
react.useEffect(function () { groupsRef.current = groups; }, [groups]);
|
|
2653
|
+
// reads the outline groups via the ref declared with the other hooks above
|
|
2033
2654
|
var loadedGroupRow = function (turn) {
|
|
2034
2655
|
var list = groupsRef.current || [];
|
|
2035
2656
|
for (var i = 0; i < list.length; i++) {
|
|
@@ -2238,9 +2859,13 @@ window.__ModuleLoader__.load({
|
|
|
2238
2859
|
// the dock edge, clipped by the sidebar line (looks covered, not
|
|
2239
2860
|
// dissolving) and only fades at the very end. No box-shadow: a shadow
|
|
2240
2861
|
// would get cut in half by the clip-path, so the panel is flat.
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2862
|
+
// A scale change moves the box's LOCAL lengths (they are divided by the factor)
|
|
2863
|
+
// while `zoom` itself does not animate: leaving `left` in the transition on that
|
|
2864
|
+
// one render would let the panel slide across the screen. It lands on exactly the
|
|
2865
|
+
// same visual spot either way, so that render simply skips the transition.
|
|
2866
|
+
var slide = open ? "0.6s " + EASE : "0.28s " + EASE;
|
|
2867
|
+
var panelTransition = (zoomJustChanged ? "" : "left " + slide + ", ")
|
|
2868
|
+
+ "clip-path " + slide + ", opacity " + (open ? "0.45s ease" : "0.14s ease 0.26s");
|
|
2244
2869
|
|
|
2245
2870
|
// clip the panel at the dock edge while collapsed, so sliding away looks
|
|
2246
2871
|
// like being covered by the sidebar (the sidebar stays untouched):
|
|
@@ -2252,9 +2877,9 @@ window.__ModuleLoader__.load({
|
|
|
2252
2877
|
if (!open) {
|
|
2253
2878
|
if (dockRight) {
|
|
2254
2879
|
// mirror of LEFT: clip the RIGHT side (panel flies right, shrinking from its right)
|
|
2255
|
-
panelClip = "inset(0 " + panelW + "
|
|
2880
|
+
panelClip = "inset(0 " + zLen(panelW) + " 0 0)";
|
|
2256
2881
|
} else {
|
|
2257
|
-
panelClip = "inset(0 0 0 " + (panelW + 8) + "
|
|
2882
|
+
panelClip = "inset(0 0 0 " + zLen(panelW + 8) + ")";
|
|
2258
2883
|
}
|
|
2259
2884
|
}
|
|
2260
2885
|
// faded out entirely while another center-column view is active
|
|
@@ -2268,11 +2893,12 @@ window.__ModuleLoader__.load({
|
|
|
2268
2893
|
var panelEl = react_jsx_runtime.jsx("div", {
|
|
2269
2894
|
style: {
|
|
2270
2895
|
position: "fixed",
|
|
2271
|
-
top: baseTopPx
|
|
2272
|
-
left: panelLeft
|
|
2273
|
-
width: panelW
|
|
2274
|
-
maxHeight: vMaxH,
|
|
2275
|
-
height: panelH > 0 ? panelH
|
|
2896
|
+
top: zLen(baseTopPx),
|
|
2897
|
+
left: zLen(panelLeft),
|
|
2898
|
+
width: zLen(panelW),
|
|
2899
|
+
maxHeight: zLen(vMaxH),
|
|
2900
|
+
height: panelH > 0 ? zLen(panelH) : undefined,
|
|
2901
|
+
zoom: zoom === 1 ? undefined : zoom,
|
|
2276
2902
|
display: "flex",
|
|
2277
2903
|
flexDirection: "column",
|
|
2278
2904
|
background: C.panelBg,
|
|
@@ -2322,16 +2948,20 @@ window.__ModuleLoader__.load({
|
|
|
2322
2948
|
ref: handleRef,
|
|
2323
2949
|
style: {
|
|
2324
2950
|
width: 56,
|
|
2951
|
+
maxWidth: "calc(100% - 12px)",
|
|
2325
2952
|
height: 5,
|
|
2326
2953
|
borderRadius: 999,
|
|
2327
2954
|
cornerShape: "round",
|
|
2328
2955
|
background: "var(--dsw-alias-border-l2, rgba(128,128,128,0.55))",
|
|
2329
|
-
|
|
2956
|
+
// no slack left in the header -> the grab hint fades away
|
|
2957
|
+
opacity: crowded ? 0 : 0.35,
|
|
2330
2958
|
transition: "opacity 0.2s ease",
|
|
2331
2959
|
margin: "5px auto 2px"
|
|
2332
2960
|
}
|
|
2333
2961
|
}),
|
|
2334
2962
|
react_jsx_runtime.jsx("div", {
|
|
2963
|
+
ref: headerRef,
|
|
2964
|
+
className: "dqt-header",
|
|
2335
2965
|
style: {
|
|
2336
2966
|
display: "flex",
|
|
2337
2967
|
alignItems: "center",
|
|
@@ -2340,7 +2970,14 @@ window.__ModuleLoader__.load({
|
|
|
2340
2970
|
borderBottom: "1px solid " + C.panelBorder,
|
|
2341
2971
|
fontSize: 13,
|
|
2342
2972
|
fontWeight: 600,
|
|
2343
|
-
gap: 6
|
|
2973
|
+
gap: 6,
|
|
2974
|
+
// the four controls keep their size and stay on ONE line; when the row
|
|
2975
|
+
// runs out of room it scrolls sideways (wheel) instead of wrapping or
|
|
2976
|
+
// overlapping, and the flexible spacer below collapses first
|
|
2977
|
+
flexWrap: "nowrap",
|
|
2978
|
+
overflowX: "auto",
|
|
2979
|
+
overflowY: "hidden",
|
|
2980
|
+
scrollbarWidth: "none"
|
|
2344
2981
|
},
|
|
2345
2982
|
children: [
|
|
2346
2983
|
// LEFT pair: the heading-level filter and the dock toggle take the
|
|
@@ -2403,8 +3040,9 @@ window.__ModuleLoader__.load({
|
|
|
2403
3040
|
iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 })
|
|
2404
3041
|
]
|
|
2405
3042
|
}),
|
|
2406
|
-
// spacer: nothing here, it just pins the two pairs to the two ends
|
|
2407
|
-
|
|
3043
|
+
// spacer: nothing here, it just pins the two pairs to the two ends —
|
|
3044
|
+
// and it is the first thing to give way when the panel narrows
|
|
3045
|
+
react_jsx_runtime.jsx("div", { style: { flex: "1 1 auto", minWidth: 0 } }),
|
|
2408
3046
|
// RIGHT pair: search + collapse
|
|
2409
3047
|
react_jsx_runtime.jsx("div", {
|
|
2410
3048
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
@@ -2787,13 +3425,15 @@ window.__ModuleLoader__.load({
|
|
|
2787
3425
|
}),
|
|
2788
3426
|
toastEl,
|
|
2789
3427
|
// resize handles (right edge: width, bottom edge: height)
|
|
3428
|
+
// Their grab strips keep their SCREEN thickness under the scale factor:
|
|
3429
|
+
// they are part of the panel's box, not of its magnified content.
|
|
2790
3430
|
react_jsx_runtime.jsx("div", {
|
|
2791
3431
|
style: {
|
|
2792
3432
|
position: "absolute",
|
|
2793
3433
|
top: 0,
|
|
2794
3434
|
bottom: 0,
|
|
2795
3435
|
right: 0,
|
|
2796
|
-
width: 8,
|
|
3436
|
+
width: zLen(8),
|
|
2797
3437
|
cursor: "col-resize",
|
|
2798
3438
|
touchAction: "none",
|
|
2799
3439
|
zIndex: 2
|
|
@@ -2807,7 +3447,7 @@ window.__ModuleLoader__.load({
|
|
|
2807
3447
|
left: 0,
|
|
2808
3448
|
right: 0,
|
|
2809
3449
|
bottom: 0,
|
|
2810
|
-
height: 8,
|
|
3450
|
+
height: zLen(8),
|
|
2811
3451
|
cursor: "row-resize",
|
|
2812
3452
|
touchAction: "none",
|
|
2813
3453
|
zIndex: 2
|
|
@@ -2821,8 +3461,8 @@ window.__ModuleLoader__.load({
|
|
|
2821
3461
|
position: "absolute",
|
|
2822
3462
|
right: 0,
|
|
2823
3463
|
bottom: 0,
|
|
2824
|
-
width: 16,
|
|
2825
|
-
height: 16,
|
|
3464
|
+
width: zLen(16),
|
|
3465
|
+
height: zLen(16),
|
|
2826
3466
|
cursor: "nwse-resize",
|
|
2827
3467
|
touchAction: "none",
|
|
2828
3468
|
zIndex: 3
|
|
@@ -3240,7 +3880,7 @@ window.__ModuleLoader__.load({
|
|
|
3240
3880
|
bottom: 0,
|
|
3241
3881
|
background: C.groupTint,
|
|
3242
3882
|
opacity: isActive ? 1 : 0,
|
|
3243
|
-
transition: "opacity 0.
|
|
3883
|
+
transition: "opacity 0.4s ease",
|
|
3244
3884
|
pointerEvents: "none"
|
|
3245
3885
|
}
|
|
3246
3886
|
}, "g-h-tint-" + gi),
|
|
@@ -3441,7 +4081,7 @@ window.__ModuleLoader__.load({
|
|
|
3441
4081
|
// the box fades in AND out: colour transitions only, and the left accent
|
|
3442
4082
|
// keeps its 3px width in both states so activating a group never reflows
|
|
3443
4083
|
// (a width change would shift every row by 2px)
|
|
3444
|
-
transition: "opacity 0.
|
|
4084
|
+
transition: "opacity 0.4s ease, background-color 0.4s ease, border-color 0.4s ease",
|
|
3445
4085
|
backgroundColor: isActive ? C.groupTint : "transparent",
|
|
3446
4086
|
borderTop: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
3447
4087
|
borderRight: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
@@ -3457,23 +4097,255 @@ window.__ModuleLoader__.load({
|
|
|
3457
4097
|
return out;
|
|
3458
4098
|
}
|
|
3459
4099
|
|
|
3460
|
-
// Host-facing locale namespace: the slot declares `locale: "dsh-quick-toc"`,
|
|
3461
|
-
//
|
|
3462
|
-
//
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
var
|
|
3467
|
-
|
|
3468
|
-
|
|
4100
|
+
// Host-facing locale namespace: the slot declares `locale: "dsh-quick-toc"`, and
|
|
4101
|
+
// registering BOTH tables under the same keys is what lets the panel follow the
|
|
4102
|
+
// host's language: `T` asks the host translation first and only falls back to the
|
|
4103
|
+
// built-in table. Keep the two hand-written tables below in sync with DICTS — the
|
|
4104
|
+
// offline suite asserts that every DICTS key exists in both languages, and the
|
|
4105
|
+
// host-facing tables are derived from DICTS so they cannot drift.
|
|
4106
|
+
var zh = {};
|
|
4107
|
+
var en = {};
|
|
4108
|
+
(function () {
|
|
4109
|
+
for (var key in DICTS.zh) {
|
|
4110
|
+
if (!Object.prototype.hasOwnProperty.call(DICTS.zh, key)) continue;
|
|
4111
|
+
zh[key] = DICTS.zh[key];
|
|
4112
|
+
en[key] = DICTS.en[key] === undefined ? DICTS.zh[key] : DICTS.en[key];
|
|
4113
|
+
}
|
|
4114
|
+
})();
|
|
4115
|
+
|
|
4116
|
+
// ---------- plugin-configuration card ----------
|
|
4117
|
+
// Settings → Plugins → Plugin configuration renders one collapsible card per
|
|
4118
|
+
// served settings namespace; this is ours, keyed on the namespace the host half
|
|
4119
|
+
// registers. It edits the SAME store the panel writes to, so the two sides are
|
|
4120
|
+
// live in both directions. The chrome mirrors the section's own card (radii,
|
|
4121
|
+
// borders, spacing, tokens) because a plugin bundle cannot import the section's
|
|
4122
|
+
// CSS module.
|
|
4123
|
+
function TocSettingsCard() {
|
|
4124
|
+
var prefs = usePrefs();
|
|
4125
|
+
setLanguage(prefs.lang, hostLanguage());
|
|
4126
|
+
var snap = prefsStore.host();
|
|
4127
|
+
var writable = !!snap && snap.writable === true;
|
|
4128
|
+
var user = snap && snap.user && typeof snap.user === "object" ? snap.user : {};
|
|
4129
|
+
var openState = react.useState(false);
|
|
4130
|
+
var open = openState[0];
|
|
4131
|
+
var setOpen = openState[1];
|
|
4132
|
+
var commit = function (patch) { prefsStore.set(patch); };
|
|
4133
|
+
var resetField = function (key) { prefsStore.unset(key); };
|
|
4134
|
+
// joined segments: one outer frame, the options split by thin dividers —
|
|
4135
|
+
// the same control shape the fonttune card uses (heights, fonts, surfaces)
|
|
4136
|
+
var seg = function (options, current, onPick) {
|
|
4137
|
+
return react_jsx_runtime.jsx("div", {
|
|
4138
|
+
className: "dqt-seg",
|
|
4139
|
+
children: options.map(function (pair) {
|
|
4140
|
+
var on = current === pair[0];
|
|
4141
|
+
return react_jsx_runtime.jsx("button", {
|
|
4142
|
+
type: "button",
|
|
4143
|
+
"aria-pressed": on,
|
|
4144
|
+
disabled: !writable,
|
|
4145
|
+
onClick: function () { onPick(pair[0]); },
|
|
4146
|
+
className: "dqt-segBtn" + (on ? " dqt-segOn" : ""),
|
|
4147
|
+
children: pair[1]
|
|
4148
|
+
}, pair[0]);
|
|
4149
|
+
})
|
|
4150
|
+
});
|
|
4151
|
+
};
|
|
4152
|
+
// the boolean rows use the same joined segments (开启/关闭), not a checkbox
|
|
4153
|
+
var onoff = function (on, onToggle) {
|
|
4154
|
+
return seg([["on", T("settings.on")], ["off", T("settings.off")]], on ? "on" : "off",
|
|
4155
|
+
function (v) { onToggle(v === "on"); });
|
|
4156
|
+
};
|
|
4157
|
+
// Scale slider, 50%..200% in 5% notches — the same control shape as the fonttune
|
|
4158
|
+
// card's slider. Dragging only moves a LOCAL value: the settings document is
|
|
4159
|
+
// written once the pointer is released (a per-notch rewrite would recompute the
|
|
4160
|
+
// panel mid-drag), and the local value stays on screen until the host confirms it,
|
|
4161
|
+
// so the readout never bounces back to the old number for a frame.
|
|
4162
|
+
var _sZoomDrag = react.useState(null);
|
|
4163
|
+
var zoomDrag = _sZoomDrag[0];
|
|
4164
|
+
var setZoomDrag = _sZoomDrag[1];
|
|
4165
|
+
var zoomAwaiting = react.useRef(null);
|
|
4166
|
+
var commitZoom = function (pct) {
|
|
4167
|
+
zoomAwaiting.current = pct;
|
|
4168
|
+
commit({ zoom: pct / 100 });
|
|
4169
|
+
};
|
|
4170
|
+
react.useEffect(function () {
|
|
4171
|
+
if (zoomAwaiting.current === null) { setZoomDrag(null); return; }
|
|
4172
|
+
if (Math.round(prefs.zoom * 100) === zoomAwaiting.current) {
|
|
4173
|
+
zoomAwaiting.current = null;
|
|
4174
|
+
setZoomDrag(null);
|
|
4175
|
+
}
|
|
4176
|
+
}, [prefs.zoom]);
|
|
4177
|
+
react.useEffect(function () {
|
|
4178
|
+
if (zoomDrag === null) return undefined;
|
|
4179
|
+
var release = function () {
|
|
4180
|
+
// one release per drag: a late pointerup must not rewrite the same value
|
|
4181
|
+
if (zoomAwaiting.current !== null) return;
|
|
4182
|
+
commitZoom(zoomDrag);
|
|
4183
|
+
};
|
|
4184
|
+
window.addEventListener("pointerup", release, true);
|
|
4185
|
+
window.addEventListener("touchend", release, true);
|
|
4186
|
+
return function () {
|
|
4187
|
+
window.removeEventListener("pointerup", release, true);
|
|
4188
|
+
window.removeEventListener("touchend", release, true);
|
|
4189
|
+
};
|
|
4190
|
+
}, [zoomDrag]);
|
|
4191
|
+
var zoomPct = (zoomDrag === null ? Math.round(prefs.zoom * 100) : zoomDrag);
|
|
4192
|
+
var zoomControl = react_jsx_runtime.jsxs("div", {
|
|
4193
|
+
children: [
|
|
4194
|
+
react_jsx_runtime.jsxs("div", {
|
|
4195
|
+
className: "dqt-sliderRow",
|
|
4196
|
+
children: [
|
|
4197
|
+
react_jsx_runtime.jsx("input", {
|
|
4198
|
+
type: "range",
|
|
4199
|
+
className: "dqt-slider",
|
|
4200
|
+
min: 50,
|
|
4201
|
+
max: 200,
|
|
4202
|
+
step: 5,
|
|
4203
|
+
value: zoomPct,
|
|
4204
|
+
disabled: !writable,
|
|
4205
|
+
"aria-label": T("settings.zoom"),
|
|
4206
|
+
onChange: function (e) { setZoomDrag(Number(e.target.value)); },
|
|
4207
|
+
onKeyUp: function () { if (zoomDrag !== null) commitZoom(zoomDrag); },
|
|
4208
|
+
onBlur: function () { if (zoomDrag !== null) commitZoom(zoomDrag); }
|
|
4209
|
+
}),
|
|
4210
|
+
react_jsx_runtime.jsx("span", { className: "dqt-sliderValue", children: zoomPct + "%" })
|
|
4211
|
+
]
|
|
4212
|
+
}),
|
|
4213
|
+
react_jsx_runtime.jsxs("div", {
|
|
4214
|
+
className: "dqt-sliderScale",
|
|
4215
|
+
children: [
|
|
4216
|
+
react_jsx_runtime.jsx("span", { children: "50%" }),
|
|
4217
|
+
react_jsx_runtime.jsx("span", { children: "200%" })
|
|
4218
|
+
]
|
|
4219
|
+
})
|
|
4220
|
+
]
|
|
4221
|
+
});
|
|
4222
|
+
var field = function (key, label, hint, control, inline, last) {
|
|
4223
|
+
var overridden = Object.prototype.hasOwnProperty.call(user, key);
|
|
4224
|
+
var reset = overridden ? react_jsx_runtime.jsx("button", {
|
|
4225
|
+
type: "button",
|
|
4226
|
+
className: "dqt-pfieldReset",
|
|
4227
|
+
disabled: !writable,
|
|
4228
|
+
onClick: function () { resetField(key); },
|
|
4229
|
+
children: T("settings.resetField")
|
|
4230
|
+
}) : null;
|
|
4231
|
+
return react_jsx_runtime.jsxs("div", {
|
|
4232
|
+
className: "dqt-pfield" + (last ? " dqt-pfieldLast" : ""),
|
|
4233
|
+
children: [
|
|
4234
|
+
react_jsx_runtime.jsxs("div", {
|
|
4235
|
+
className: "dqt-pfieldHead",
|
|
4236
|
+
children: [
|
|
4237
|
+
react_jsx_runtime.jsx("span", { className: "dqt-pfieldLabel", children: label }),
|
|
4238
|
+
overridden ? react_jsx_runtime.jsx("span", { className: "dqt-poverridden", children: T("settings.overridden") }) : null,
|
|
4239
|
+
reset,
|
|
4240
|
+
inline ? react_jsx_runtime.jsx("span", { className: "dqt-pinline", children: control }) : null
|
|
4241
|
+
]
|
|
4242
|
+
}),
|
|
4243
|
+
hint ? react_jsx_runtime.jsx("p", { className: "dqt-phint", children: hint }) : null,
|
|
4244
|
+
inline ? null : control
|
|
4245
|
+
]
|
|
4246
|
+
}, key);
|
|
4247
|
+
};
|
|
4248
|
+
var levelsControl = function (inline) {
|
|
4249
|
+
return react_jsx_runtime.jsx("div", {
|
|
4250
|
+
className: "dqt-pseg" + (inline ? " dqt-psegInline" : ""),
|
|
4251
|
+
children: LEVELS_ALL.map(function (lv) {
|
|
4252
|
+
var on = prefs.levels.indexOf(lv) !== -1;
|
|
4253
|
+
return react_jsx_runtime.jsx("button", {
|
|
4254
|
+
type: "button",
|
|
4255
|
+
disabled: !writable,
|
|
4256
|
+
onClick: function () {
|
|
4257
|
+
var next;
|
|
4258
|
+
if (on) {
|
|
4259
|
+
next = prefs.levels.filter(function (x) { return x !== lv; });
|
|
4260
|
+
if (next.length === 0) next = LEVELS_ALL.slice(); // never leave the panel empty
|
|
4261
|
+
} else {
|
|
4262
|
+
next = LEVELS_ALL.filter(function (x) { return x === lv || prefs.levels.indexOf(x) !== -1; });
|
|
4263
|
+
}
|
|
4264
|
+
commit({ levels: next });
|
|
4265
|
+
},
|
|
4266
|
+
title: (on ? T("levels.hide") : T("levels.show")) + lv,
|
|
4267
|
+
className: "dqt-psegBtn" + (on ? " dqt-psegOn" : ""),
|
|
4268
|
+
children: "H" + lv
|
|
4269
|
+
}, "lv-" + lv);
|
|
4270
|
+
})
|
|
4271
|
+
});
|
|
4272
|
+
};
|
|
4273
|
+
var fields = [
|
|
4274
|
+
field("lang", T("settings.language"), T("settings.language.tip"),
|
|
4275
|
+
seg([["auto", T("settings.lang.auto")], ["zh", T("settings.lang.zh")], ["en", T("settings.lang.en")]], prefs.lang,
|
|
4276
|
+
function (v) { commit({ lang: v }); }), true),
|
|
4277
|
+
field("dock", T("settings.dock"), "",
|
|
4278
|
+
seg([["left", T("settings.dock.left")], ["right", T("settings.dock.right")]], prefs.dock,
|
|
4279
|
+
function (v) { commit({ dock: v }); }), true),
|
|
4280
|
+
field("levels", T("settings.levels"), "", levelsControl(true), true),
|
|
4281
|
+
field("zoom", T("settings.zoom"), T("settings.zoom.tip"), zoomControl, false),
|
|
4282
|
+
field("fuzzy", T("settings.fuzzy"), "", onoff(prefs.fuzzy, function (v) { commit({ fuzzy: v }); }), true),
|
|
4283
|
+
field("hover", T("settings.hover"), "", onoff(prefs.hover, function (v) { commit({ hover: v }); }), true),
|
|
4284
|
+
field("debug", T("settings.debug"), T("settings.debug.tip"), onoff(prefs.debug, function (v) { commit({ debug: v }); }), true, true)
|
|
4285
|
+
];
|
|
4286
|
+
return react_jsx_runtime.jsxs("li", {
|
|
4287
|
+
className: "dqt-pcard" + (open ? " dqt-pcardOpen" : ""),
|
|
4288
|
+
children: [
|
|
4289
|
+
react_jsx_runtime.jsxs("button", {
|
|
4290
|
+
type: "button",
|
|
4291
|
+
className: "dqt-phead",
|
|
4292
|
+
"aria-expanded": open,
|
|
4293
|
+
onClick: function () { setOpen(!open); },
|
|
4294
|
+
children: [
|
|
4295
|
+
react_jsx_runtime.jsxs("span", { className: "dqt-pheadText", children: [
|
|
4296
|
+
react_jsx_runtime.jsx("span", { className: "dqt-pname", children: T("panel.title") }),
|
|
4297
|
+
react_jsx_runtime.jsx("span", { className: "dqt-pdesc", children: T("settings.desc") })
|
|
4298
|
+
] }),
|
|
4299
|
+
react_jsx_runtime.jsx("span", { className: "dqt-pchev" + (open ? " dqt-pchevOpen" : ""), "aria-hidden": "true", children: "⌄" })
|
|
4300
|
+
]
|
|
4301
|
+
}),
|
|
4302
|
+
open ? react_jsx_runtime.jsx("div", {
|
|
4303
|
+
className: "dqt-pbody",
|
|
4304
|
+
children: [
|
|
4305
|
+
writable ? null : react_jsx_runtime.jsx("p", { className: "dqt-preadOnly", role: "status", children: T("settings.readOnly") }),
|
|
4306
|
+
fields
|
|
4307
|
+
]
|
|
4308
|
+
}) : null
|
|
4309
|
+
]
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
3469
4312
|
|
|
3470
|
-
var inject = ["slots", "locale"];
|
|
4313
|
+
var inject = ["slots", "locale", "settingsScope"];
|
|
3471
4314
|
|
|
3472
4315
|
function apply(ctx) {
|
|
3473
4316
|
ctx.effect(function () {
|
|
3474
4317
|
return ctx.locale.register("dsh-quick-toc", { zh: zh, en: en });
|
|
3475
4318
|
}, "dsh-quick-toc: dictionaries");
|
|
3476
4319
|
|
|
4320
|
+
// The host's translate function for our namespace is what makes "follow the
|
|
4321
|
+
// host" work: `T` prefers it, so the panel speaks whatever language DSH is in.
|
|
4322
|
+
// Optional on purpose — a host without a locale service leaves the built-in
|
|
4323
|
+
// table in charge.
|
|
4324
|
+
if (ctx.locale && typeof ctx.locale.bind === "function") {
|
|
4325
|
+
try {
|
|
4326
|
+
var bound = ctx.locale.bind("dsh-quick-toc");
|
|
4327
|
+
hostTranslate = function (key) {
|
|
4328
|
+
try {
|
|
4329
|
+
var s = bound(key);
|
|
4330
|
+
// the host echoes the key back for anything it cannot translate, and the
|
|
4331
|
+
// built-in table is the better fallback in that case
|
|
4332
|
+
return typeof s === "string" && s !== key ? s : undefined;
|
|
4333
|
+
} catch (e) {
|
|
4334
|
+
return undefined;
|
|
4335
|
+
}
|
|
4336
|
+
};
|
|
4337
|
+
} catch (e) {
|
|
4338
|
+
hostTranslate = null;
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
// Two open DSH tabs should agree: the other tab's write to one of these keys
|
|
4343
|
+
// arrives here as a `storage` event, and the store re-reads and notifies, so the
|
|
4344
|
+
// panel and the configuration card both follow along.
|
|
4345
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
4346
|
+
window.addEventListener("storage", function () { prefsStore.reload(); });
|
|
4347
|
+
}
|
|
4348
|
+
|
|
3477
4349
|
// Bridge to the host session face, used to open a turn whose events the
|
|
3478
4350
|
// paged window has not loaded: ctx.sessions.binding(id).session.loadThrough(seq)
|
|
3479
4351
|
// is the host's documented "page history backwards until the window covers
|
|
@@ -3512,6 +4384,36 @@ window.__ModuleLoader__.load({
|
|
|
3512
4384
|
return OutlinePanel(Object.assign({}, props, { tocHost: host }));
|
|
3513
4385
|
});
|
|
3514
4386
|
});
|
|
4387
|
+
|
|
4388
|
+
// The client settings scope: the browser face of the namespace the host half
|
|
4389
|
+
// registers. The store adopts its snapshot as the authoritative layer for the
|
|
4390
|
+
// host-backed fields and routes writes to it; on a host without the service (or
|
|
4391
|
+
// on a non-loopback page, where DSH keeps settings read-only) the localStorage
|
|
4392
|
+
// mirror keeps every preference working, exactly as 0.5.x did.
|
|
4393
|
+
var boundScope = null;
|
|
4394
|
+
if (ctx.settingsScope && typeof ctx.settingsScope.bind === "function") {
|
|
4395
|
+
try {
|
|
4396
|
+
boundScope = ctx.settingsScope.bind({ namespace: NAMESPACE });
|
|
4397
|
+
prefsStore.attachHost(boundScope);
|
|
4398
|
+
} catch (e) {
|
|
4399
|
+
boundScope = null;
|
|
4400
|
+
console.warn("[dsh-quick-toc] settings scope unavailable, preferences stay in this browser:", e);
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
// The plugin-configuration card: Settings → Plugins → Plugin configuration, a
|
|
4405
|
+
// keyed slot dispatched only for namespaces the Host actually serves. Registering
|
|
4406
|
+
// it only when the scope bound keeps a half-configured host from rendering a card
|
|
4407
|
+
// that could not read or write anything.
|
|
4408
|
+
if (boundScope) {
|
|
4409
|
+
ctx.slots.inject("settings.plugin.item", function () {
|
|
4410
|
+
return ctx.slots.register({
|
|
4411
|
+
name: "settings.plugin.item",
|
|
4412
|
+
key: NAMESPACE,
|
|
4413
|
+
locale: "dsh-quick-toc"
|
|
4414
|
+
}, TocSettingsCard);
|
|
4415
|
+
});
|
|
4416
|
+
}
|
|
3515
4417
|
}
|
|
3516
4418
|
|
|
3517
4419
|
exports.name = "dsh-quick-toc"; // runner attributes slot entries to this registrant
|
|
@@ -3549,7 +4451,57 @@ window.__ModuleLoader__.load({
|
|
|
3549
4451
|
".dqt-list::-webkit-scrollbar-button{display:none;height:0;width:0}" +
|
|
3550
4452
|
".dqt-list::-webkit-scrollbar-track{background:transparent}" +
|
|
3551
4453
|
".dqt-list::-webkit-scrollbar-thumb{background:transparent}" +
|
|
3552
|
-
".dqt-list::-webkit-scrollbar-corner{background:transparent}"
|
|
4454
|
+
".dqt-list::-webkit-scrollbar-corner{background:transparent}" +
|
|
4455
|
+
// the header scrolls sideways once the four controls fill it: the native bar
|
|
4456
|
+
// would be taller than the row, so it is hidden (the wheel still scrolls)
|
|
4457
|
+
".dqt-header{scrollbar-width:none;-ms-overflow-style:none}" +
|
|
4458
|
+
".dqt-header::-webkit-scrollbar{display:none;width:0;height:0}" +
|
|
4459
|
+
".dqt-header::-webkit-scrollbar-thumb{background:transparent}" +
|
|
4460
|
+
// Plugin-configuration card (Settings → Plugins → Plugin configuration).
|
|
4461
|
+
// The chrome reproduces the section's own card — a plugin bundle cannot
|
|
4462
|
+
// import that CSS module — with the same tokens, radii and spacing.
|
|
4463
|
+
".dqt-pcard{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);border-radius:16px;list-style:none;transition:border-color .16s,background .16s}" +
|
|
4464
|
+
".dqt-pcard:hover{border-color:var(--dsw-alias-label-dimmed)}" +
|
|
4465
|
+
".dqt-pcardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}" +
|
|
4466
|
+
".dqt-phead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}" +
|
|
4467
|
+
".dqt-phead:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}" +
|
|
4468
|
+
".dqt-pheadText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}" +
|
|
4469
|
+
".dqt-pname{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}" +
|
|
4470
|
+
".dqt-pdesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}" +
|
|
4471
|
+
".dqt-pchev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}" +
|
|
4472
|
+
".dqt-pchevOpen{transform:rotate(180deg)}" +
|
|
4473
|
+
".dqt-pbody{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}" +
|
|
4474
|
+
".dqt-preadOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}" +
|
|
4475
|
+
".dqt-pfield{padding:14px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}" +
|
|
4476
|
+
".dqt-pfieldLast{border-bottom:0}" +
|
|
4477
|
+
".dqt-pfieldHead{align-items:center;gap:8px;display:flex}" +
|
|
4478
|
+
".dqt-pfieldLabel{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}" +
|
|
4479
|
+
".dqt-poverridden{flex:none;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:999px;padding:1px 8px;font-size:11px;line-height:16px}" +
|
|
4480
|
+
".dqt-pfieldReset{flex:none;appearance:none;font:inherit;cursor:pointer;background:0 0;border:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding:0}" +
|
|
4481
|
+
".dqt-pfieldReset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}" +
|
|
4482
|
+
".dqt-pfieldReset:disabled{opacity:.4;cursor:default}" +
|
|
4483
|
+
".dqt-pinline{flex:1;justify-content:flex-end;align-items:center;gap:10px;display:flex}" +
|
|
4484
|
+
".dqt-phint{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:18px}" +
|
|
4485
|
+
".dqt-pseg{align-items:center;flex-wrap:wrap;gap:6px;margin-top:10px;display:flex}" +
|
|
4486
|
+
".dqt-psegInline{margin-top:0}" +
|
|
4487
|
+
".dqt-psegBtn{appearance:none;font:inherit;cursor:pointer;height:28px;padding:0 12px;color:var(--dsw-alias-label-secondary);background:0 0;border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;font-size:13px;line-height:18px}" +
|
|
4488
|
+
".dqt-psegBtn:hover:not(:disabled):not(.dqt-psegOn){background:var(--dsw-specific-sidebar-nav-item-hover)}" +
|
|
4489
|
+
".dqt-psegBtn:disabled{opacity:.4;cursor:default}" +
|
|
4490
|
+
".dqt-psegOn{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-sidebar-nav-item-active);border-color:transparent}" +
|
|
4491
|
+
// joined segmented control — one outer frame, options split by thin
|
|
4492
|
+
// dividers; same heights, fonts and surfaces as the fonttune card's
|
|
4493
|
+
".dqt-seg{flex:none;display:inline-flex;overflow:hidden;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:8px}" +
|
|
4494
|
+
".dqt-segBtn{appearance:none;font:inherit;cursor:pointer;height:28px;padding:0 14px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-left:.5px solid var(--dsw-alias-border-l3);font-size:13px;line-height:18px}" +
|
|
4495
|
+
".dqt-segBtn:first-child{border-left:none}" +
|
|
4496
|
+
".dqt-segBtn:hover:not(:disabled):not(.dqt-segOn){background:var(--dsw-specific-sidebar-nav-item-hover)}" +
|
|
4497
|
+
".dqt-segBtn:disabled{opacity:.4;cursor:default}" +
|
|
4498
|
+
".dqt-segOn{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-sidebar-nav-item-active)}" +
|
|
4499
|
+
// scale slider — the fonttune card's slider shape (track, readout, end labels)
|
|
4500
|
+
".dqt-sliderRow{align-items:center;gap:10px;margin-top:10px;display:flex}" +
|
|
4501
|
+
".dqt-slider{flex:1;min-width:0;height:20px;accent-color:var(--dsw-alias-brand-primary)}" +
|
|
4502
|
+
".dqt-slider:disabled{opacity:.4;cursor:default}" +
|
|
4503
|
+
".dqt-sliderValue{flex:none;min-width:56px;text-align:right;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;font-variant-numeric:tabular-nums}" +
|
|
4504
|
+
".dqt-sliderScale{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;display:flex;justify-content:space-between}";
|
|
3553
4505
|
document.head.appendChild(s);
|
|
3554
4506
|
})();
|
|
3555
4507
|
|