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/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. Kept as a module-level binding so
72
- // the module-scope render helpers (renderItem/renderResultRow/...) can localise
73
- // without threading a `t` argument through every call.
74
- var activeDict = DICTS.zh;
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
- var s = activeDict[key];
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' (persisted; migrates old dsh-contents /
796
- // dsh-dagang keys, default left)
797
- var DOCK_KEY = "dsh-quick-toc.dock.v2";
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
- var next = dock === "right" ? "left" : "right";
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 (persisted; migrates old dsh-contents / dsh-dagang keys)
819
- var PANEL_Y_KEY = "dsh-quick-toc.panelY.v1";
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 (persisted; width default 288, height 0 = auto)
855
- var PANEL_W_KEY = "dsh-quick-toc.panelW.v1";
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 (persisted) ----
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 LEVELS_KEY = "dsh-quick-toc.levels.v1";
875
- var OLD_MAX_LEVEL_KEY = "dsh-quick-toc.maxLevel.v1";
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 FUZZY_KEY = "dsh-quick-toc.fuzzy.v1";
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
- var next = !fuzzy;
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
- var next = Math.max(180, Math.min(560, startW + (ev.clientX - startX)));
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
- var next = Math.max(160, Math.min(window.innerHeight - 60, startH + (ev.clientY - startY)));
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
- var w = Math.max(180, Math.min(560, startW + (ev.clientX - startX)));
1378
- var h = Math.max(160, Math.min(window.innerHeight - 60, startH + (ev.clientY - startY)));
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 -> Date (handles ms / s epochs / ISO strings) ----
1397
- var toDate = function (t) {
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 fmtStamp(t);
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 with
1582
- // localStorage.setItem("dsh-quick-toc.debug", "1")
1583
- // plus a reload. Kept because it is the fastest way to tell "the host has no
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
- }, [outlineTurns]);
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
- if (handleRef.current) handleRef.current.style.opacity = on ? "1" : "0.35";
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
- // A SMOOTH scroll is only trustworthy while the paged conversation keeps its
1963
- // geometry. A LONG jump travels through the whole window, and the host mounts
1964
- // and drops turns along the way, so the target's offset moves mid-flight and
1965
- // the animation lands short ("it scrolled halfway and stopped"). Long jumps
1966
- // therefore position INSTANTLY and then re-check for a short while (the same
1967
- // "instant, never smooth" rule the outline's own follow uses), while nearby
1968
- // headings keep the glide. When glued to the bottom, lift just past DSH's 25px
1969
- // stick-to-bottom threshold first so the landing is not yanked back.
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
- el.scrollIntoView({ behavior: "smooth", block: "start" });
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(t - sp.scrollTop) > 60) {
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
- if (Math.abs(t - sp.scrollTop) <= sp.clientHeight * 1.5) {
1987
- sp.scrollTo({ top: t, behavior: "smooth" });
1988
- return;
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
- // long jump: land now, then keep the landing honest for ~0.6s while the host
1991
- // finishes measuring the window it just re-paged
1992
- sp.scrollTop = t;
1993
- var ticks = 0;
1994
- var settle = setInterval(function () {
1995
- ticks++;
1996
- var next = scrollTargetOf(el, sp);
1997
- if (Math.abs(next - sp.scrollTop) > 2) sp.scrollTop = next;
1998
- if (ticks >= 5) clearInterval(settle);
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
- // Fallback locator for the poll below: once the paged window covers the
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
- var panelTransition = open
2242
- ? "left 0.6s " + EASE + ", clip-path 0.6s " + EASE + ", opacity 0.45s ease"
2243
- : "left 0.28s " + EASE + ", clip-path 0.28s " + EASE + ", opacity 0.14s ease 0.26s";
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 + "px 0 0)";
2880
+ panelClip = "inset(0 " + zLen(panelW) + " 0 0)";
2256
2881
  } else {
2257
- panelClip = "inset(0 0 0 " + (panelW + 8) + "px)";
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 + "px",
2272
- left: panelLeft + "px",
2273
- width: panelW + "px",
2274
- maxHeight: vMaxH,
2275
- height: panelH > 0 ? panelH + "px" : undefined,
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
- opacity: 0.35,
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
- react_jsx_runtime.jsx("div", { style: { flex: "1 1 auto" } }),
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.18s ease",
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.15s ease, background-color 0.18s ease, border-color 0.18s ease",
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
- // which is what hands the panel its `t` prop. The panel's own visible strings
3462
- // live in DICTS and are kept in sync with this table.
3463
- var zh = {
3464
- "panel.title": DICTS.zh["panel.title"]
3465
- };
3466
- var en = {
3467
- "panel.title": "Conversation Outline"
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