dsh-quick-toc 0.7.0 → 0.7.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 +20 -0
- package/CHANGELOG.md +20 -0
- package/README.en.md +4 -2
- package/README.md +4 -2
- package/lib/client.js +862 -133
- package/lib/index.js +4 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -128,8 +128,10 @@ window.__ModuleLoader__.load({
|
|
|
128
128
|
"search.empty": "没有匹配",
|
|
129
129
|
"search.hintMore": "向上滚动可加载更早的消息",
|
|
130
130
|
"search.hintOldest": "已经是最早的消息",
|
|
131
|
+
"hint.autoOff": "上面还有更早的历史(自动加载已关闭,点「未加载」的条目才会加载)",
|
|
131
132
|
"hint.bottom": "已经到底了",
|
|
132
133
|
"outline.bottom": "回到底部",
|
|
134
|
+
"outline.backToMine": "回到我滚到的位置",
|
|
133
135
|
"turn.jumpReply": "跳转到该回合的模型回答开头",
|
|
134
136
|
"row.end": "跳到本节末尾",
|
|
135
137
|
"turn.jumpTurn": "跳转到该回合开头",
|
|
@@ -164,7 +166,7 @@ window.__ModuleLoader__.load({
|
|
|
164
166
|
"settings.fuzzy": "模糊搜索",
|
|
165
167
|
"settings.hover": "悬停预览卡片",
|
|
166
168
|
"settings.remember": "记住阅读位置",
|
|
167
|
-
"settings.
|
|
169
|
+
"settings.autoLoad": "自动加载历史",
|
|
168
170
|
"settings.debug": "在控制台打印诊断日志",
|
|
169
171
|
"settings.debug.tip": "开启后控制台会打印一行挂载诊断(宿主能力与跳转加载器状态)",
|
|
170
172
|
"settings.overridden": "已自定义",
|
|
@@ -220,8 +222,10 @@ window.__ModuleLoader__.load({
|
|
|
220
222
|
"search.empty": "No matches",
|
|
221
223
|
"search.hintMore": "Scroll up to load earlier messages",
|
|
222
224
|
"search.hintOldest": "This is the earliest message",
|
|
225
|
+
"hint.autoOff": "There is older history above (auto-loading is off — click an unloaded row to load it)",
|
|
223
226
|
"hint.bottom": "You have reached the end",
|
|
224
227
|
"outline.bottom": "Back to the newest entry",
|
|
228
|
+
"outline.backToMine": "Back to where I scrolled to",
|
|
225
229
|
"turn.jumpReply": "Jump to the start of this turn's model reply",
|
|
226
230
|
"row.end": "Jump to the end of this section",
|
|
227
231
|
"turn.jumpTurn": "Jump to the start of this turn",
|
|
@@ -258,7 +262,7 @@ window.__ModuleLoader__.load({
|
|
|
258
262
|
"settings.fuzzy": "Fuzzy search",
|
|
259
263
|
"settings.hover": "Hover preview card",
|
|
260
264
|
"settings.remember": "Remember the reading position",
|
|
261
|
-
"settings.
|
|
265
|
+
"settings.autoLoad": "Load history automatically",
|
|
262
266
|
"settings.debug": "Print the mount diagnostic to the console",
|
|
263
267
|
"settings.debug.tip": "The console then prints one mount line (host capabilities and jump-loader state)",
|
|
264
268
|
"settings.overridden": "Customized",
|
|
@@ -331,7 +335,7 @@ window.__ModuleLoader__.load({
|
|
|
331
335
|
// purpose — they describe THIS screen, not the account.
|
|
332
336
|
var NAMESPACE = "dsh-quick-toc";
|
|
333
337
|
// Fields backed by the Host settings document; everything else is localStorage.
|
|
334
|
-
var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, zoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1 };
|
|
338
|
+
var HOST_FIELDS = { dock: 1, lang: 1, levels: 1, zoom: 1, handle: 1, fuzzy: 1, hover: 1, debug: 1, remember: 1, autoLoad: 1 };
|
|
335
339
|
// The schema defaults (mirrored here for the card's per-field reset and for the
|
|
336
340
|
// one-time import of a 0.5.x install's stored values).
|
|
337
341
|
var DEFAULTS = {
|
|
@@ -339,7 +343,7 @@ window.__ModuleLoader__.load({
|
|
|
339
343
|
levels: [1, 2, 3, 4, 5, 6],
|
|
340
344
|
zoom: 1, handle: 0.5,
|
|
341
345
|
fuzzy: false, hover: true, lang: "auto", debug: false,
|
|
342
|
-
remember: true
|
|
346
|
+
remember: true, autoLoad: true
|
|
343
347
|
};
|
|
344
348
|
var PREF_KEYS = {
|
|
345
349
|
dock: "dsh-quick-toc.dock.v2",
|
|
@@ -353,6 +357,7 @@ window.__ModuleLoader__.load({
|
|
|
353
357
|
hover: "dsh-quick-toc.hover.v1",
|
|
354
358
|
lang: "dsh-quick-toc.lang.v1",
|
|
355
359
|
remember: "dsh-quick-toc.remember.v1",
|
|
360
|
+
autoLoad: "dsh-quick-toc.autoLoad.v1",
|
|
356
361
|
debug: "dsh-quick-toc.debug"
|
|
357
362
|
};
|
|
358
363
|
// Writing a field's schema default through the card IS its reset — a value that
|
|
@@ -484,6 +489,8 @@ window.__ModuleLoader__.load({
|
|
|
484
489
|
hover: read(PREF_KEYS.hover) !== "0",
|
|
485
490
|
// absent means "on" too (the reader asked for this to default on)
|
|
486
491
|
remember: read(PREF_KEYS.remember) !== "0",
|
|
492
|
+
// and so does the history pull that follows the reader through the outline
|
|
493
|
+
autoLoad: read(PREF_KEYS.autoLoad) !== "0",
|
|
487
494
|
lang: langRaw === "zh" || langRaw === "en" ? langRaw : "auto",
|
|
488
495
|
debug: read(PREF_KEYS.debug) === "1"
|
|
489
496
|
};
|
|
@@ -500,6 +507,7 @@ window.__ModuleLoader__.load({
|
|
|
500
507
|
fuzzy: function (v) { return v ? "1" : "0"; },
|
|
501
508
|
hover: function (v) { return v ? "1" : "0"; },
|
|
502
509
|
remember: function (v) { return v ? "1" : "0"; },
|
|
510
|
+
autoLoad: function (v) { return v ? "1" : "0"; },
|
|
503
511
|
lang: function (v) { return v; },
|
|
504
512
|
debug: function (v) { return v ? "1" : "0"; }
|
|
505
513
|
};
|
|
@@ -531,6 +539,7 @@ window.__ModuleLoader__.load({
|
|
|
531
539
|
fuzzy: v.fuzzy === true,
|
|
532
540
|
hover: v.hover !== false,
|
|
533
541
|
remember: v.remember !== false,
|
|
542
|
+
autoLoad: v.autoLoad !== false,
|
|
534
543
|
debug: v.debug === true
|
|
535
544
|
};
|
|
536
545
|
};
|
|
@@ -541,7 +550,7 @@ window.__ModuleLoader__.load({
|
|
|
541
550
|
return {
|
|
542
551
|
dock: over.dock, y: local.y, w: local.w, h: local.h,
|
|
543
552
|
levels: over.levels, zoom: over.zoom, handle: over.handle, fuzzy: over.fuzzy, hover: over.hover,
|
|
544
|
-
lang: over.lang, remember: over.remember, debug: over.debug
|
|
553
|
+
lang: over.lang, remember: over.remember, debug: over.debug, autoLoad: over.autoLoad
|
|
545
554
|
};
|
|
546
555
|
};
|
|
547
556
|
var notify = function () {
|
|
@@ -1573,6 +1582,7 @@ window.__ModuleLoader__.load({
|
|
|
1573
1582
|
var openFromEdge = function () {
|
|
1574
1583
|
if (open) return;
|
|
1575
1584
|
setEdgeLeaving(true);
|
|
1585
|
+
armRestore("dock"); // the position the reader closed the panel at, if any
|
|
1576
1586
|
setOpen(true);
|
|
1577
1587
|
};
|
|
1578
1588
|
react.useEffect(function () {
|
|
@@ -1677,6 +1687,27 @@ window.__ModuleLoader__.load({
|
|
|
1677
1687
|
var outlineTouchRef = react.useRef(0); // last time the user touched the outline
|
|
1678
1688
|
var lastScrollTopRef = react.useRef(0);
|
|
1679
1689
|
var lastHostClickRef = react.useRef(0); // throttle for the "load earlier" click
|
|
1690
|
+
// Auto-load bookkeeping (see the paging block): at most one pull in flight, a beat
|
|
1691
|
+
// between pulls, and a per-gesture budget so a runaway loop can never swallow the
|
|
1692
|
+
// whole log. The switch itself is mirrored into a ref because a scheduled retry
|
|
1693
|
+
// fires from an older render's closure.
|
|
1694
|
+
var pullBusyRef = react.useRef(false);
|
|
1695
|
+
var autoTimerRef = react.useRef(null);
|
|
1696
|
+
var autoBurstRef = react.useRef(0);
|
|
1697
|
+
var autoBusyRef = react.useRef(0);
|
|
1698
|
+
var autoWatchRef = react.useRef(null); // did the pull we started actually land? (fallback click)
|
|
1699
|
+
var autoProbeRef = react.useRef(0); // throttle for the on-scroll approach probe
|
|
1700
|
+
var autoStampRef = react.useRef(0);
|
|
1701
|
+
var autoLoadRef = react.useRef(true);
|
|
1702
|
+
var panelVisibleRef = react.useRef(true);
|
|
1703
|
+
// The reader's place in the outline: sampled by the scroll handler and right
|
|
1704
|
+
// before any change of our own, restored in a LAYOUT effect (see the paging
|
|
1705
|
+
// block). `anchorHintRef` remembers where the visible boundary was, so finding it
|
|
1706
|
+
// again stays cheap on a list with hundreds of groups.
|
|
1707
|
+
var anchorRef = react.useRef(null);
|
|
1708
|
+
var anchorHintRef = react.useRef(0);
|
|
1709
|
+
// one confirmation loop at a time, cancelled on unmount (see confirmPlace)
|
|
1710
|
+
var keepPlaceRafRef = react.useRef(null);
|
|
1680
1711
|
|
|
1681
1712
|
// ---- "back to the newest row" button ------------------------------------
|
|
1682
1713
|
// The list can be scrolled far up into history (that is the whole point of
|
|
@@ -1713,6 +1744,110 @@ window.__ModuleLoader__.load({
|
|
|
1713
1744
|
else el.scrollTop = el.scrollHeight;
|
|
1714
1745
|
};
|
|
1715
1746
|
|
|
1747
|
+
// ---- "back to where I was when I closed this" ---------------------------
|
|
1748
|
+
// The outline re-centres itself on the turn being read (the follow), and opening a surface
|
|
1749
|
+
// lands on that turn — so a reader who had scrolled the outline somewhere of their own
|
|
1750
|
+
// accord finds it somewhere else afterwards, with no way back. The memory is taken AT
|
|
1751
|
+
// CLOSE TIME and kept PER SURFACE (the docked panel and the curtain are different shapes
|
|
1752
|
+
// and are left at different rows): closing the panel remembers the panel's row, closing
|
|
1753
|
+
// the curtain remembers the curtain's, and opening one of them arms a one-shot button in
|
|
1754
|
+
// the top-right corner FOR THAT SURFACE — the two memories are stored apart and can never
|
|
1755
|
+
// overwrite each other. Using the button — or simply scrolling back there yourself —
|
|
1756
|
+
// consumes that surface's memory and fades the button out, and nothing is remembered
|
|
1757
|
+
// again until the next close.
|
|
1758
|
+
//
|
|
1759
|
+
// What is remembered is the ROW that sat at the top AND where it sat, not a bare pixel
|
|
1760
|
+
// offset: the same list is re-laid out between close and open (widths, curtain handoff,
|
|
1761
|
+
// turns that finish loading), so an offset alone would drift off the row the reader was
|
|
1762
|
+
// actually looking at — but the row alone is not enough either. At the bottom of the list
|
|
1763
|
+
// the row on the top edge is usually CLIPPED (its own top sits above the list's), and
|
|
1764
|
+
// measuring "how far is that row from the top" against 0 made the button light up the
|
|
1765
|
+
// moment the reader reopened a list they had left at the bottom — and then walk them up by
|
|
1766
|
+
// that clipping distance. Comparing the row's offset NOW with the offset it had when they
|
|
1767
|
+
// left answers the real question ("is this a different place?"), and with zero drift
|
|
1768
|
+
// nothing is offered and nothing moves.
|
|
1769
|
+
var closedPosRef = react.useRef({ dock: null, sheet: null }); // recorded at close time
|
|
1770
|
+
var livePosRef = react.useRef({ dock: null, sheet: null }); // armed when that surface opens
|
|
1771
|
+
var _sRestore = react.useState(0);
|
|
1772
|
+
var setRestoreTick = _sRestore[1];
|
|
1773
|
+
// Which surface the reader is looking at right now: the button speaks for that one, and
|
|
1774
|
+
// only that one's memory is ever spent.
|
|
1775
|
+
var surfaceNow = function () { return sheetOpen ? "sheet" : "dock"; };
|
|
1776
|
+
var liveTgt = function () { return livePosRef.current[surfaceNow()]; };
|
|
1777
|
+
var spendRestore = function () {
|
|
1778
|
+
var s = surfaceNow();
|
|
1779
|
+
if (livePosRef.current[s] === null) return;
|
|
1780
|
+
livePosRef.current[s] = null;
|
|
1781
|
+
setRestoreTick(function (n) { return n + 1; }); // the button fades out
|
|
1782
|
+
};
|
|
1783
|
+
var noteClosed = function (which) {
|
|
1784
|
+
var el = listRef.current;
|
|
1785
|
+
if (!el) return;
|
|
1786
|
+
var a = anchorAt(el); // the row at the top of the list right now
|
|
1787
|
+
closedPosRef.current[which] = a
|
|
1788
|
+
? { idx: a.idx, off: a.top - el.getBoundingClientRect().top }
|
|
1789
|
+
: null;
|
|
1790
|
+
};
|
|
1791
|
+
var armRestore = function (which) {
|
|
1792
|
+
var m = closedPosRef.current[which];
|
|
1793
|
+
closedPosRef.current[which] = null; // one shot: this open consumes it
|
|
1794
|
+
livePosRef.current[which] = (m && m.idx !== undefined) ? m : null;
|
|
1795
|
+
if (m && m.idx !== undefined) {
|
|
1796
|
+
// let the restore win over the follow for a moment: otherwise the follow re-aligns
|
|
1797
|
+
// the READING row the instant the panel is back and eats the memory
|
|
1798
|
+
outlineTouchRef.current = Date.now();
|
|
1799
|
+
}
|
|
1800
|
+
setRestoreTick(function (n) { return n + 1; });
|
|
1801
|
+
};
|
|
1802
|
+
// How far that row has drifted from where the reader left it (0 = still the same place)
|
|
1803
|
+
var restoreDrift = function (el) {
|
|
1804
|
+
var tgt = liveTgt();
|
|
1805
|
+
if (!el || !tgt) return null;
|
|
1806
|
+
var node = el.querySelector('[data-group-idx="' + tgt.idx + '"]');
|
|
1807
|
+
if (!node) return null; // that row is gone: nothing to go back to
|
|
1808
|
+
return (node.getBoundingClientRect().top - el.getBoundingClientRect().top) - tgt.off;
|
|
1809
|
+
};
|
|
1810
|
+
var awayFromRestore = function (el) {
|
|
1811
|
+
var d = restoreDrift(el);
|
|
1812
|
+
return d !== null && Math.abs(d) > 40;
|
|
1813
|
+
};
|
|
1814
|
+
var syncBackBtn = function (el) {
|
|
1815
|
+
var d = restoreDrift(el);
|
|
1816
|
+
// the reader brought the row back to where it was by hand: that is the same as using it
|
|
1817
|
+
if (d !== null && Math.abs(d) <= 40) spendRestore();
|
|
1818
|
+
};
|
|
1819
|
+
var backToBrowse = function (e) {
|
|
1820
|
+
if (e && e.stopPropagation) e.stopPropagation();
|
|
1821
|
+
outlineTouchRef.current = Date.now();
|
|
1822
|
+
hoverEnd();
|
|
1823
|
+
var el = listRef.current;
|
|
1824
|
+
var tgt = liveTgt();
|
|
1825
|
+
if (!el || !tgt) return;
|
|
1826
|
+
var node = el.querySelector('[data-group-idx="' + tgt.idx + '"]');
|
|
1827
|
+
if (node) {
|
|
1828
|
+
// put that row back where it SAT (not necessarily at the top): a row the reader left
|
|
1829
|
+
// clipped by 120px goes home clipped by 120px, so the view lands exactly where it was
|
|
1830
|
+
var d = (node.getBoundingClientRect().top - el.getBoundingClientRect().top) - tgt.off;
|
|
1831
|
+
if (Math.abs(d) > 0.5) el.scrollTop = el.scrollTop + d;
|
|
1832
|
+
}
|
|
1833
|
+
syncAtBottom(el);
|
|
1834
|
+
spendRestore();
|
|
1835
|
+
};
|
|
1836
|
+
// The button floats over the LIST, so it has to clear whatever chrome sits above the
|
|
1837
|
+
// list — and that chrome differs between the docked panel (grip bar + header row, plus
|
|
1838
|
+
// the search row when open) and the curtain (its own toolbar). Measuring where the list
|
|
1839
|
+
// actually begins is exact for both, and stays right if the chrome changes height.
|
|
1840
|
+
var _sListTop = react.useState(0);
|
|
1841
|
+
var listTop = _sListTop[0];
|
|
1842
|
+
var setListTop = _sListTop[1];
|
|
1843
|
+
react.useLayoutEffect(function () {
|
|
1844
|
+
var root = panelRootRef.current;
|
|
1845
|
+
var el = listRef.current;
|
|
1846
|
+
if (!root || !el || !root.getBoundingClientRect || !el.getBoundingClientRect) return;
|
|
1847
|
+
var top = Math.round(el.getBoundingClientRect().top - root.getBoundingClientRect().top);
|
|
1848
|
+
if (top > 0 && top !== listTop) setListTop(top);
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1716
1851
|
// ---- "scroll up to load earlier messages" hint (search mode) -----------
|
|
1717
1852
|
// Armed by a search when older history is still reachable; dismissed the
|
|
1718
1853
|
// moment the reader scrolls up (which is when the older page starts loading,
|
|
@@ -1958,6 +2093,7 @@ window.__ModuleLoader__.load({
|
|
|
1958
2093
|
// re-renders until the curtain is actually gone.
|
|
1959
2094
|
var closeSheet = function () {
|
|
1960
2095
|
if (!sheetOpen || sheetTimerRef.current) return;
|
|
2096
|
+
noteClosed("sheet"); // where they were when they shut the curtain
|
|
1961
2097
|
var el = sheetRef.current;
|
|
1962
2098
|
if (el && el.classList) {
|
|
1963
2099
|
el.classList.remove("dqt-sheet-open");
|
|
@@ -1975,6 +2111,7 @@ window.__ModuleLoader__.load({
|
|
|
1975
2111
|
};
|
|
1976
2112
|
var openSheet = function () {
|
|
1977
2113
|
if (sheetOpen || panelAside) return;
|
|
2114
|
+
armRestore("sheet"); // the position the reader closed the curtain at, if any
|
|
1978
2115
|
if (sheetTimerRef.current) { clearTimeout(sheetTimerRef.current); sheetTimerRef.current = null; }
|
|
1979
2116
|
// reopening mid-retract: the node still carries the exit class and React
|
|
1980
2117
|
// would not put the enter class back on its own (that prop never changed)
|
|
@@ -2245,82 +2382,408 @@ window.__ModuleLoader__.load({
|
|
|
2245
2382
|
var setActiveGroup = _s10[1];
|
|
2246
2383
|
var activeSigRef = react.useRef("");
|
|
2247
2384
|
|
|
2248
|
-
//
|
|
2249
|
-
//
|
|
2385
|
+
// ---- history paging: what is loaded, what is not, and how to ask ------------
|
|
2386
|
+
// The loader does NOT touch the transcript. It used to nudge it 26px off DSH's
|
|
2387
|
+
// stick-to-bottom line before every pull, to stop DSH's `toBottom` re-pin from
|
|
2388
|
+
// undoing an outline-triggered page-in (0.5.1). That nudge is visible in the middle
|
|
2389
|
+
// column, and it fires from the background loader too, so the reader saw the
|
|
2390
|
+
// conversation move while they were only reading the outline ("I did not click
|
|
2391
|
+
// anything, why did the middle panel scroll up"). It is also no longer needed: the
|
|
2392
|
+
// outline's follow has its own quiet window after any outline gesture
|
|
2393
|
+
// (`outlineTouchRef`), so a re-pin cannot drag the rail to the newest turn any more,
|
|
2394
|
+
// and `glideTo` keeps its own guard for the jumps the reader actually asks for.
|
|
2395
|
+
// The session face carries the host's own bookkeeping — `openState`, `hasMore`
|
|
2396
|
+
// and `loadingOlder` — so the panel never has to infer "is there more?" from a
|
|
2397
|
+
// button it found in the DOM. That inference is exactly what used to stall the
|
|
2398
|
+
// outline: the host renders its "load earlier" button only while `hasMore` and
|
|
2399
|
+
// DISABLES it while a page is in flight, so a scroll-up arriving during a load was
|
|
2400
|
+
// read as "nothing older" — the reader was told they had reached the beginning
|
|
2401
|
+
// with hundreds of turns still above them.
|
|
2402
|
+
var sessionFace = function () {
|
|
2403
|
+
var sessions = hostSessions();
|
|
2404
|
+
var binding = sessions && typeof sessions.binding === "function" ? sessions.binding(sessionId) : null;
|
|
2405
|
+
return binding && binding.session ? binding.session : null;
|
|
2406
|
+
};
|
|
2407
|
+
var faceSettled = function (face) { return face.openState === undefined || face.openState === "open"; };
|
|
2408
|
+
var historyBusy = function (face) { return !!face && face.loadingOlder === true; };
|
|
2409
|
+
// The beginning is reached only when the window says so AND nothing is in flight:
|
|
2410
|
+
// `hasMore` is stale while a page is being pulled (it flips once that page lands),
|
|
2411
|
+
// and reading it during a pull is exactly how "still loading" became "no more
|
|
2412
|
+
// history" for the reader.
|
|
2413
|
+
var historyExhausted = function (face) {
|
|
2414
|
+
return !!face && face.hasMore === false && !historyBusy(face) && faceSettled(face);
|
|
2415
|
+
};
|
|
2416
|
+
// The host's own "load earlier" control. The API above is the normal path, but a
|
|
2417
|
+
// build can expose the session face and still ignore the pull; clicking this is
|
|
2418
|
+
// exactly what the reader would do by hand, so the auto-loader falls back to it.
|
|
2419
|
+
// (The reader asked for precisely this: "when it loads, click that for me".)
|
|
2420
|
+
var viaHostButtonRef = react.useRef(function () { return false; });
|
|
2421
|
+
viaHostButtonRef.current = function () {
|
|
2422
|
+
var btn = findLoadOlderButton();
|
|
2423
|
+
if (!btn || btn.disabled) return false;
|
|
2424
|
+
btn.click();
|
|
2425
|
+
return true;
|
|
2426
|
+
};
|
|
2427
|
+
var viaHostButton = function () { return viaHostButtonRef.current(); };
|
|
2428
|
+
// Extend the window backwards by one host page (~50 messages): cheap and bounded,
|
|
2429
|
+
// the way to walk into older history rather than swallow it.
|
|
2430
|
+
// `interactive` = the reader asked for this (their own gesture in the outline). Only
|
|
2431
|
+
// then do we nudge the TRANSCRIPT off DSH's stick-to-bottom line — that nudge is what
|
|
2432
|
+
// used to make the conversation move while the reader was only reading the outline
|
|
2433
|
+
// ("I did not click anything and the middle panel scrolled up"). A background prefetch
|
|
2434
|
+
// leaves the transcript alone: if the reader is at the newest message, DSH keeps them
|
|
2435
|
+
// there, and if they are mid-transcript the host compensates the prepend itself.
|
|
2436
|
+
var pullPage = function () {
|
|
2437
|
+
var face = sessionFace();
|
|
2438
|
+
if (!face || typeof face.loadOlder !== "function") return false;
|
|
2439
|
+
if (historyBusy(face) || historyExhausted(face)) return false;
|
|
2440
|
+
try {
|
|
2441
|
+
var p = face.loadOlder();
|
|
2442
|
+
if (p && typeof p.catch === "function") p.catch(function () {});
|
|
2443
|
+
} catch (e) { return false; }
|
|
2444
|
+
return true;
|
|
2445
|
+
};
|
|
2446
|
+
|
|
2447
|
+
// ---- holding the reader's place while the list reflows ----------------------
|
|
2448
|
+
// Any load changes the list's height ABOVE the reader: the index reveals older
|
|
2449
|
+
// groups at the top, and a group that loads swaps two lines of preview for a whole
|
|
2450
|
+
// subtree. The list is a top-anchored flow, so a box can only grow downward — which
|
|
2451
|
+
// is exactly why the reader was pushed: everything under the grown box moves down.
|
|
2452
|
+
// The cure is not to grow the box upward (document flow cannot do that) but to take
|
|
2453
|
+
// that push back out of `scrollTop` in the SAME frame, which is indistinguishable
|
|
2454
|
+
// from growing upward: nothing the reader is looking at moves, the new rows simply
|
|
2455
|
+
// appear above the viewport. The anchor is the first group box in view, and
|
|
2456
|
+
// `data-group-idx` survives a ghost turning real (the group list does not change
|
|
2457
|
+
// length when a turn loads).
|
|
2458
|
+
// Scanning for it starts at the last known boundary: a full scan per scroll event
|
|
2459
|
+
// reads every rect in the list, which is what made long outlines stutter.
|
|
2460
|
+
var anchorAt = function (el) {
|
|
2461
|
+
var boxes = el.querySelectorAll("[data-group-idx]");
|
|
2462
|
+
if (!boxes || boxes.length === 0) return null;
|
|
2463
|
+
var lr = el.getBoundingClientRect();
|
|
2464
|
+
var limit = lr.top + 1;
|
|
2465
|
+
var i0 = anchorHintRef.current;
|
|
2466
|
+
if (i0 > boxes.length - 1) i0 = boxes.length - 1;
|
|
2467
|
+
if (i0 < 0) i0 = 0;
|
|
2468
|
+
// Walk from where the boundary was last seen — scrolling is gradual, so this reads
|
|
2469
|
+
// one or two rects per call. `i` ends up on the row that straddles the top edge.
|
|
2470
|
+
var i = i0;
|
|
2471
|
+
var r = boxes[i].getBoundingClientRect();
|
|
2472
|
+
while (r.bottom <= limit && i + 1 < boxes.length) { i++; r = boxes[i].getBoundingClientRect(); }
|
|
2473
|
+
while (r.bottom > limit && i > 0) {
|
|
2474
|
+
var prev = boxes[i - 1].getBoundingClientRect();
|
|
2475
|
+
if (prev.bottom <= limit) break;
|
|
2476
|
+
i--; r = prev;
|
|
2477
|
+
}
|
|
2478
|
+
// Choose the anchor. The FIRST REAL row in view wins: the placeholders ("未加载"
|
|
2479
|
+
// rows) are exactly what loads and grows, so anchoring below them keeps every row
|
|
2480
|
+
// the reader can see exactly where it is while the unloaded block grows upward out
|
|
2481
|
+
// of the viewport — which is the behaviour the reader asked for ("grow upward"). An
|
|
2482
|
+
// anchor ON a placeholder would sit still while everything under it slid down, and
|
|
2483
|
+
// an anchor on a row that straddles the top edge measures no growth at all (its own
|
|
2484
|
+
// top does not move). Falls back to the first fully visible row, then to whatever is
|
|
2485
|
+
// visible at the top. Bounded by what is on screen, so a few rects at most.
|
|
2486
|
+
var pick = i, pickTop = r.top, seenReal = false, seenFull = false;
|
|
2487
|
+
for (var k = i; k < boxes.length && k < i + 80; k++) {
|
|
2488
|
+
var rk = (k === i) ? r : boxes[k].getBoundingClientRect();
|
|
2489
|
+
if (rk.top >= lr.bottom) break; // past the bottom edge
|
|
2490
|
+
var ghostAttr = boxes[k].getAttribute ? boxes[k].getAttribute("data-ghost-turn") : null;
|
|
2491
|
+
var isGhost = ghostAttr !== null && ghostAttr !== undefined;
|
|
2492
|
+
if (!isGhost && !seenReal) { pick = k; pickTop = rk.top; seenReal = true; break; }
|
|
2493
|
+
if (isGhost && rk.top >= limit && !seenFull) { pick = k; pickTop = rk.top; seenFull = true; }
|
|
2494
|
+
}
|
|
2495
|
+
anchorHintRef.current = pick;
|
|
2496
|
+
return { idx: boxes[pick].getAttribute("data-group-idx"), top: pickTop };
|
|
2497
|
+
};
|
|
2498
|
+
// What the reader is looking at, and where it sits. Sampled by the scroll handler —
|
|
2499
|
+
// so a host-side change landing mid-scroll is still caught — and right before any
|
|
2500
|
+
// change we start ourselves.
|
|
2501
|
+
var sampleAnchor = function (el) {
|
|
2502
|
+
var a = el ? anchorAt(el) : null;
|
|
2503
|
+
anchorRef.current = a ? { idx: a.idx, top: a.top, scrollTop: el.scrollTop } : null;
|
|
2504
|
+
return anchorRef.current;
|
|
2505
|
+
};
|
|
2506
|
+
var restoreAnchor = function (el) {
|
|
2507
|
+
var a = anchorRef.current;
|
|
2508
|
+
if (!a || !el || !el.isConnected) return false;
|
|
2509
|
+
var node = el.querySelector('[data-group-idx="' + a.idx + '"]');
|
|
2510
|
+
if (!node) { sampleAnchor(el); return false; }
|
|
2511
|
+
var top = node.getBoundingClientRect().top;
|
|
2512
|
+
var moved = el.scrollTop - a.scrollTop; // what the reader (or a jump) scrolled
|
|
2513
|
+
// Growth above the anchor moves it DOWN by exactly as much as the content grew; the
|
|
2514
|
+
// reader scrolling up moves it down by exactly as much as they scrolled. Both happen
|
|
2515
|
+
// at once when a page lands mid-gesture, so subtract the reader's part rather than
|
|
2516
|
+
// refusing to compensate — the old "scrollTop changed, so the reader moved, skip"
|
|
2517
|
+
// guard is exactly why a load landing during a scroll was still seen pushing the
|
|
2518
|
+
// list. (The list carries `overflow-anchor: none`, so the browser is not adjusting
|
|
2519
|
+
// `scrollTop` behind our back and this sum stays honest.)
|
|
2520
|
+
var grew = (top - a.top) + moved;
|
|
2521
|
+
if (Math.abs(grew) < 0.5 || Math.abs(grew) > (el.clientHeight || 0) * 3) {
|
|
2522
|
+
// nothing grew — or something absurd happened (a repaged list, a session swap):
|
|
2523
|
+
// re-baseline instead of chasing it
|
|
2524
|
+
a.scrollTop = el.scrollTop; a.top = top;
|
|
2525
|
+
return false;
|
|
2526
|
+
}
|
|
2527
|
+
var before = el.scrollTop;
|
|
2528
|
+
el.scrollTop = before + grew;
|
|
2529
|
+
if (Math.abs(el.scrollTop - before) < 0.5) {
|
|
2530
|
+
// clamped (the reader is already at one end): nothing to hold on to
|
|
2531
|
+
sampleAnchor(el);
|
|
2532
|
+
return false;
|
|
2533
|
+
}
|
|
2534
|
+
a.scrollTop = el.scrollTop;
|
|
2535
|
+
a.top = top - grew; // the offset the anchor keeps from now on
|
|
2536
|
+
return true;
|
|
2537
|
+
};
|
|
2538
|
+
var holdPlace = function (el) { sampleAnchor(el); };
|
|
2539
|
+
// A host page lands over several ticks (window → snapshot → React render), so the
|
|
2540
|
+
// layout effect above is re-checked for a few frames — bounded, and it stops as soon
|
|
2541
|
+
// as the height settles, so an idle list costs nothing. Only ONE such loop may run:
|
|
2542
|
+
// a second one would correct the same content twice.
|
|
2543
|
+
var confirmPlace = function (el) {
|
|
2544
|
+
if (keepPlaceRafRef.current) cancelAnimationFrame(keepPlaceRafRef.current);
|
|
2545
|
+
var n = 0, lastH = -1;
|
|
2546
|
+
var step = function () {
|
|
2547
|
+
if (!el.isConnected || n > 8) { keepPlaceRafRef.current = null; return; }
|
|
2548
|
+
n++;
|
|
2549
|
+
restoreAnchor(el);
|
|
2550
|
+
var h = el.scrollHeight;
|
|
2551
|
+
if (h === lastH) { keepPlaceRafRef.current = null; return; }
|
|
2552
|
+
lastH = h;
|
|
2553
|
+
keepPlaceRafRef.current = requestAnimationFrame(step);
|
|
2554
|
+
};
|
|
2555
|
+
keepPlaceRafRef.current = requestAnimationFrame(step);
|
|
2556
|
+
};
|
|
2557
|
+
// ---- auto-load: follow the reader into the part that is not loaded ----------
|
|
2558
|
+
// The reader asked for this by name: "when I scroll into a gap, load it" — and,
|
|
2559
|
+
// just as firmly, "nothing at all when I turn it off". So the trigger is geometric
|
|
2560
|
+
// (an unloaded group that is actually in view) and the switch is consulted first.
|
|
2561
|
+
var AUTO_PACE_MS = 130; // beat while merely PREFETCHING (nothing to catch up to)
|
|
2562
|
+
var AUTO_WAIT_MS = 40; // beat while a page is in flight: the host is the limit
|
|
2563
|
+
var AUTO_BURST_MAX = 60; // runaway guard only: a long gap must stay walkable
|
|
2564
|
+
var AUTO_BURST_IDLE_MS = 3000; // quiet this long and the budget is fresh again
|
|
2565
|
+
var ghostInView = function (el) {
|
|
2566
|
+
var ghosts = el.querySelectorAll("[data-ghost-turn]");
|
|
2567
|
+
if (ghosts.length === 0) return null;
|
|
2568
|
+
var box = el.getBoundingClientRect();
|
|
2569
|
+
for (var i = 0; i < ghosts.length; i++) {
|
|
2570
|
+
var r = ghosts[i].getBoundingClientRect();
|
|
2571
|
+
if (r.bottom > box.top && r.top < box.bottom) return Number(ghosts[i].getAttribute("data-ghost-turn"));
|
|
2572
|
+
}
|
|
2573
|
+
return null;
|
|
2574
|
+
};
|
|
2575
|
+
|
|
2576
|
+
// How far AHEAD of the reader the gap is filled: while the bottom edge of the
|
|
2577
|
+
// nearest unloaded turn is still within this many viewports above the viewport's top
|
|
2578
|
+
// edge, the next page is fetched — so by the time the reader scrolls to where the
|
|
2579
|
+
// unloaded turns are, they are already real. The growth then happens ABOVE the view,
|
|
2580
|
+
// where nothing they are looking at can move; that is the trick that removes the seam
|
|
2581
|
+
// without animating anything.
|
|
2582
|
+
//
|
|
2583
|
+
// Measuring the GHOST (not the loaded/unloaded frontier) is what gives this its
|
|
2584
|
+
// hysteresis: the frontier group sits at the viewport's top edge by definition, so
|
|
2585
|
+
// "the frontier is within a viewport" was true no matter how much had just loaded,
|
|
2586
|
+
// and the loader pulled another page every beat until the whole log was in memory —
|
|
2587
|
+
// continuous host work, which is the stutter the reader hit while sitting still. A
|
|
2588
|
+
// ghost's distance, by contrast, grows by exactly the height of each page that lands,
|
|
2589
|
+
// so the chain stops once a page has put a viewport of real content above the reader.
|
|
2590
|
+
var PREFETCH_AHEAD = 1.2;
|
|
2591
|
+
var firstRealIndex = function () {
|
|
2592
|
+
var list = groupsRef.current || [];
|
|
2593
|
+
for (var i = 0; i < list.length; i++) if (!list[i].ghost) return i;
|
|
2594
|
+
return -1;
|
|
2595
|
+
};
|
|
2596
|
+
// The seq the host needs to page a turn in. The outline carries it for every turn
|
|
2597
|
+
// (loaded or not), because the host's projection and the jump loader both speak seq.
|
|
2598
|
+
var seqOfTurn = function (turn) {
|
|
2599
|
+
var list = groupsRef.current || [];
|
|
2600
|
+
for (var i = 0; i < list.length; i++) {
|
|
2601
|
+
if (list[i] && list[i].turn === turn) return typeof list[i].seq === "number" ? list[i].seq : null;
|
|
2602
|
+
}
|
|
2603
|
+
return null;
|
|
2604
|
+
};
|
|
2605
|
+
// Jump the window straight to a turn the reader has ARRIVED at. `loadThrough(seq)` lets
|
|
2606
|
+
// the host page its own history (200 messages a hop) until the window covers that seq,
|
|
2607
|
+
// so a deep gap closes in ONE call — the reader's complaint was exactly this ("I have
|
|
2608
|
+
// already scrolled into the unloaded part and it is still loading slowly"). Paging
|
|
2609
|
+
// 50 messages at a time stays the fallback, and stays the rule while merely
|
|
2610
|
+
// prefetching ahead of them, where there is nothing to catch up to.
|
|
2611
|
+
//
|
|
2612
|
+
// The target is a MARGIN further back than the row in view: jumping to exactly that one
|
|
2613
|
+
// turn left the next "未加载" row sitting immediately above it, so the reader saw the gap
|
|
2614
|
+
// again as soon as they kept scrolling. Covering `AUTO_JUMP_MARGIN` rows at a time makes
|
|
2615
|
+
// one jump serve the next stretch of scrolling too.
|
|
2616
|
+
var AUTO_JUMP_MARGIN = 8;
|
|
2617
|
+
var jumpSeqFor = function (turn) {
|
|
2618
|
+
var list = groupsRef.current || [];
|
|
2619
|
+
var at = -1;
|
|
2620
|
+
for (var i = 0; i < list.length; i++) if (list[i] && list[i].turn === turn) { at = i; break; }
|
|
2621
|
+
if (at < 0) return seqOfTurn(turn);
|
|
2622
|
+
for (var k = Math.max(0, at - AUTO_JUMP_MARGIN); k <= at; k++) {
|
|
2623
|
+
if (list[k] && typeof list[k].seq === "number") return list[k].seq;
|
|
2624
|
+
}
|
|
2625
|
+
return seqOfTurn(turn);
|
|
2626
|
+
};
|
|
2627
|
+
var loadThroughTurn = function (turn) {
|
|
2628
|
+
var face = sessionFace();
|
|
2629
|
+
if (!face || typeof face.loadThrough !== "function") return false;
|
|
2630
|
+
var seq = jumpSeqFor(turn);
|
|
2631
|
+
if (seq === null) return false;
|
|
2632
|
+
if (historyBusy(face) || historyExhausted(face)) return false;
|
|
2633
|
+
try {
|
|
2634
|
+
var p = face.loadThrough(seq);
|
|
2635
|
+
if (p && typeof p.catch === "function") p.catch(function () {});
|
|
2636
|
+
} catch (e) { return false; }
|
|
2637
|
+
return true;
|
|
2638
|
+
};
|
|
2639
|
+
var frontierApproaching = function (el) {
|
|
2640
|
+
// Read the UNLOADED rows by their own attribute. (They also carry
|
|
2641
|
+
// `data-group-idx`, but asking for the placeholder directly says what this is
|
|
2642
|
+
// about — and it is fewer boxes to measure.)
|
|
2643
|
+
var ghosts = el.querySelectorAll ? el.querySelectorAll("[data-ghost-turn]") : [];
|
|
2644
|
+
var box = el.getBoundingClientRect();
|
|
2645
|
+
var ahead = (box.height || 0) * PREFETCH_AHEAD;
|
|
2646
|
+
var above = null; // distance from the nearest unloaded turn up to the viewport
|
|
2647
|
+
for (var i = 0; i < ghosts.length; i++) {
|
|
2648
|
+
var r = ghosts[i].getBoundingClientRect();
|
|
2649
|
+
if (r.bottom > box.top) return true; // unloaded content is in view
|
|
2650
|
+
above = box.top - r.bottom; // the last ghost is the nearest one
|
|
2651
|
+
}
|
|
2652
|
+
return above !== null && above < ahead;
|
|
2653
|
+
};
|
|
2654
|
+
var autoRetry = function (fast) {
|
|
2655
|
+
if (autoTimerRef.current) return;
|
|
2656
|
+
autoTimerRef.current = setTimeout(function () {
|
|
2657
|
+
autoTimerRef.current = null;
|
|
2658
|
+
pullBusyRef.current = false;
|
|
2659
|
+
var el = listRef.current;
|
|
2660
|
+
if (!el) return;
|
|
2661
|
+
// If our own pull changed nothing while the host still has older history, this
|
|
2662
|
+
// build is not honouring the API pull: fall back to the host's own control, once
|
|
2663
|
+
// per attempt. The reader asked for exactly this ("click that for me").
|
|
2664
|
+
var w = autoWatchRef.current;
|
|
2665
|
+
if (w) {
|
|
2666
|
+
var moved = firstRealIndex() !== w.frontier;
|
|
2667
|
+
if (moved) autoWatchRef.current = null;
|
|
2668
|
+
else {
|
|
2669
|
+
w.tries = (w.tries || 0) + 1;
|
|
2670
|
+
// three beats (≈0.4s) with nothing moving means this build is not landing our
|
|
2671
|
+
// pull: click the host's own control once instead of waiting forever
|
|
2672
|
+
if (!w.clicked && w.tries >= 3) {
|
|
2673
|
+
var face2 = sessionFace();
|
|
2674
|
+
if (!face2 || !historyBusy(face2)) w.clicked = viaHostButton() || true;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
autoLoadStep(el);
|
|
2679
|
+
}, fast ? AUTO_WAIT_MS : AUTO_PACE_MS);
|
|
2680
|
+
};
|
|
2681
|
+
var autoLoadStep = function (el) {
|
|
2682
|
+
if (!autoLoadRef.current || !el || !el.isConnected) return false;
|
|
2683
|
+
if (pullBusyRef.current) return true; // a pull is in flight: still progressing
|
|
2684
|
+
var now = Date.now();
|
|
2685
|
+
if (now - autoStampRef.current > AUTO_BURST_IDLE_MS) autoBurstRef.current = 0;
|
|
2686
|
+
if (autoBurstRef.current >= AUTO_BURST_MAX) return false;
|
|
2687
|
+
var face = sessionFace();
|
|
2688
|
+
var ghostTurn = ghostInView(el);
|
|
2689
|
+
if (!face) {
|
|
2690
|
+
// no session service (older host): the host's own control is all there is
|
|
2691
|
+
if (el.scrollTop <= 24 && findLoadOlderButton() && viaHostButton()) {
|
|
2692
|
+
autoBurstRef.current++; autoStampRef.current = now; return true;
|
|
2693
|
+
}
|
|
2694
|
+
return false;
|
|
2695
|
+
}
|
|
2696
|
+
if (historyBusy(face)) {
|
|
2697
|
+
// a page is in flight: WAIT for it, on a SHORT beat, so the next one starts as soon
|
|
2698
|
+
// as the host is done with this one (the host, not our pacing, should be the limit).
|
|
2699
|
+
// Reading "in flight" as "no more history" is the bug that stopped the loading.
|
|
2700
|
+
// These waits are not pages, so they do not spend the page budget — but they are
|
|
2701
|
+
// capped, so a genuinely stuck pull cannot spin here.
|
|
2702
|
+
autoBusyRef.current++;
|
|
2703
|
+
if (autoBusyRef.current <= 60) autoRetry(true);
|
|
2704
|
+
return true;
|
|
2705
|
+
}
|
|
2706
|
+
if (historyExhausted(face)) return false;
|
|
2707
|
+
// A ghost the reader can actually see, or the gap coming up on them.
|
|
2708
|
+
if (ghostTurn !== null || frontierApproaching(el)) {
|
|
2709
|
+
// ARRIVED at an unloaded turn: cover it in one host call (`loadThrough`), which is
|
|
2710
|
+
// what the reader expects ("it should load the moment I scroll to it"). A deep gap
|
|
2711
|
+
// walked 50 messages at a time is what made it feel like it never caught up.
|
|
2712
|
+
// MERELY APPROACHING (the prefetch band): one page at a time, so history is fetched
|
|
2713
|
+
// just ahead of the reader and nothing large lands while they sit still.
|
|
2714
|
+
holdPlace(el); // remember the place BEFORE the height changes
|
|
2715
|
+
autoBurstRef.current++; autoStampRef.current = now;
|
|
2716
|
+
pullBusyRef.current = true;
|
|
2717
|
+
var started = ghostTurn !== null
|
|
2718
|
+
? (loadThroughTurn(ghostTurn) || pullPage())
|
|
2719
|
+
: pullPage();
|
|
2720
|
+
if (!started) { pullBusyRef.current = false; return false; }
|
|
2721
|
+
autoBusyRef.current = 0; // a load started: the wait cap is per stuck load
|
|
2722
|
+
// One watch across the whole fruitless streak, not one per pull: re-arming it
|
|
2723
|
+
// every beat would reset the count and the fallback would never fire.
|
|
2724
|
+
if (!autoWatchRef.current) autoWatchRef.current = { frontier: firstRealIndex(), tries: 0, clicked: false };
|
|
2725
|
+
confirmPlace(el); // a load lands over several ticks
|
|
2726
|
+
autoRetry(true);
|
|
2727
|
+
return true;
|
|
2728
|
+
}
|
|
2729
|
+
// Nothing unloaded is near, so there is nothing to fetch. There used to be an
|
|
2730
|
+
// unconditional "we are at the very top, pull anyway" branch here; it was both
|
|
2731
|
+
// redundant (at the true top the unloaded turns ARE on screen, so the branch above
|
|
2732
|
+
// fires) and harmful: it kept pulling while the reader sat still, page after page,
|
|
2733
|
+
// which is host work they never asked for.
|
|
2734
|
+
return false;
|
|
2735
|
+
};
|
|
2250
2736
|
var loadOlderOutline = function (el) {
|
|
2251
2737
|
var grow = function () {
|
|
2252
2738
|
if (visibleCount < groups.length) {
|
|
2253
|
-
|
|
2739
|
+
// Revealing older rows prepends them, so the reader's content is pushed down.
|
|
2740
|
+
// Sample first and let the layout effect take the push out again in the same
|
|
2741
|
+
// frame: correcting two frames later (the old double-rAF) IS the bounce the
|
|
2742
|
+
// reader saw when they reached the unloaded part of the list.
|
|
2743
|
+
holdPlace(el);
|
|
2254
2744
|
setVisibleCount(Math.min(groups.length, visibleCount + PAGE_SIZE));
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
});
|
|
2259
|
-
});
|
|
2745
|
+
// the revealed rows are not in the DOM yet, so re-check once after the render:
|
|
2746
|
+
// if the reveal uncovered unloaded turns, they load within the same gesture
|
|
2747
|
+
autoRetry();
|
|
2260
2748
|
return true;
|
|
2261
2749
|
}
|
|
2262
2750
|
return false;
|
|
2263
2751
|
};
|
|
2264
|
-
var
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
if (floor - sp.scrollTop <= 25) sp.scrollTop = Math.max(0, floor - 26);
|
|
2279
|
-
};
|
|
2280
|
-
var viaHost = function () {
|
|
2281
|
-
var btn = findLoadOlderButton();
|
|
2282
|
-
if (!btn || btn.disabled) return false;
|
|
2283
|
-
liftOffBottom();
|
|
2284
|
-
btn.click();
|
|
2285
|
-
// once the conversation loads more, expand the outline window too
|
|
2286
|
-
setTimeout(function () {
|
|
2287
|
-
var prevH2 = el.scrollHeight;
|
|
2288
|
-
if (grow()) {
|
|
2289
|
-
requestAnimationFrame(function () {
|
|
2290
|
-
requestAnimationFrame(function () {
|
|
2291
|
-
el.scrollTop += (el.scrollHeight - prevH2);
|
|
2292
|
-
});
|
|
2293
|
-
});
|
|
2294
|
-
}
|
|
2295
|
-
}, 1200);
|
|
2296
|
-
return true;
|
|
2297
|
-
};
|
|
2298
|
-
var viaHostThrottled = function () {
|
|
2299
|
-
var now = Date.now();
|
|
2300
|
-
if (now - lastHostClickRef.current < 900) return false;
|
|
2301
|
-
if (!findLoadOlderButton()) return false;
|
|
2302
|
-
lastHostClickRef.current = now;
|
|
2303
|
-
return viaHost();
|
|
2304
|
-
};
|
|
2305
|
-
// In SEARCH mode the list shows matches, not the outline: paging the
|
|
2306
|
-
// outline's own window would change nothing the reader can see, whereas
|
|
2307
|
-
// loading the conversation's older messages extends what the search can
|
|
2308
|
-
// cover at all (the whole-log index only carries bounded previews). So the
|
|
2309
|
-
// host's own "load earlier" comes first while a query is active.
|
|
2310
|
-
if (query.trim() !== "") return viaHost() || grow();
|
|
2311
|
-
// Outline mode keeps the ORIGINAL contract: scrolling the outline up also
|
|
2312
|
-
// pulls the conversation's older messages in (that is what makes the outline
|
|
2313
|
-
// a substitute for scrolling the transcript), not merely more index entries.
|
|
2314
|
-
// The host click is throttled so one continuous scroll cannot hammer the
|
|
2315
|
-
// pager; grow() still runs first so the index never lags behind the window.
|
|
2752
|
+
var viaHostButton = function () { return viaHostButtonRef.current(); };
|
|
2753
|
+
// In SEARCH mode the list shows matches, not the outline: paging the outline's
|
|
2754
|
+
// own window would change nothing the reader can see, whereas loading the
|
|
2755
|
+
// conversation's older messages extends what the search can cover at all (the
|
|
2756
|
+
// whole-log index only carries bounded previews). So the host pull comes first
|
|
2757
|
+
// while a query is active.
|
|
2758
|
+
if (query.trim() !== "") {
|
|
2759
|
+
holdPlace(el);
|
|
2760
|
+
var pulled = pullPage() || viaHostButton();
|
|
2761
|
+
if (pulled) confirmPlace(el);
|
|
2762
|
+
return pulled || grow();
|
|
2763
|
+
}
|
|
2764
|
+
// Outline mode: reveal more of the index, and let the auto-loader fetch the
|
|
2765
|
+
// history behind the gap the reader has scrolled into (see autoLoadStep).
|
|
2316
2766
|
var grew = grow();
|
|
2317
|
-
|
|
2767
|
+
var fetched = autoLoadStep(el);
|
|
2768
|
+
return grew || fetched;
|
|
2318
2769
|
};
|
|
2319
2770
|
|
|
2320
2771
|
// older history still reachable? the outline can always page its own window
|
|
2321
|
-
// further, and beyond that the
|
|
2772
|
+
// further, and beyond that the session face decides (see historyExhausted)
|
|
2322
2773
|
var canLoadOlder = function () {
|
|
2323
|
-
|
|
2774
|
+
var face = sessionFace();
|
|
2775
|
+
if (visibleCount < groups.length) return true;
|
|
2776
|
+
if (face) return !historyExhausted(face);
|
|
2777
|
+
return !!findLoadOlderButton();
|
|
2778
|
+
};
|
|
2779
|
+
// Positive evidence that the beginning really is reached — the ONLY state that may
|
|
2780
|
+
// claim it in the hint. A pull in flight, a spent burst budget or a switched-off
|
|
2781
|
+
// auto-loader are all "not now", never "nothing older".
|
|
2782
|
+
var atHistoryStart = function () {
|
|
2783
|
+
if (visibleCount < groups.length) return false;
|
|
2784
|
+
var face = sessionFace();
|
|
2785
|
+
if (face) return historyExhausted(face);
|
|
2786
|
+
return !findLoadOlderButton();
|
|
2324
2787
|
};
|
|
2325
2788
|
|
|
2326
2789
|
// wheel up (toward older) loads more when the list is at its top or has
|
|
@@ -2344,9 +2807,15 @@ window.__ModuleLoader__.load({
|
|
|
2344
2807
|
if (!progressed) {
|
|
2345
2808
|
if (query.trim() !== "") {
|
|
2346
2809
|
if (hint !== "oldest") setHint("oldest");
|
|
2347
|
-
} else {
|
|
2810
|
+
} else if (atHistoryStart()) {
|
|
2348
2811
|
showBanner(T("search.hintOldest"));
|
|
2812
|
+
} else if (!prefs.autoLoad) {
|
|
2813
|
+
// the honest message when the reader switched auto-loading off: there IS
|
|
2814
|
+
// more history, it simply is not being fetched behind their back
|
|
2815
|
+
showBanner(T("hint.autoOff"));
|
|
2349
2816
|
}
|
|
2817
|
+
// otherwise a page is in flight (or this gesture's budget is spent): the
|
|
2818
|
+
// silence is deliberate — "已经是最早" here is what used to lie
|
|
2350
2819
|
}
|
|
2351
2820
|
}
|
|
2352
2821
|
};
|
|
@@ -2354,6 +2823,8 @@ window.__ModuleLoader__.load({
|
|
|
2354
2823
|
// scroll to the top edge also loads older groups (keeps the visual position)
|
|
2355
2824
|
var onListScroll = function (e) {
|
|
2356
2825
|
outlineTouchRef.current = Date.now();
|
|
2826
|
+
// the scroll container itself: a real scroll event's `currentTarget` IS the list
|
|
2827
|
+
// element, which is also what the paging logic reads geometry from
|
|
2357
2828
|
var el = e.currentTarget;
|
|
2358
2829
|
syncAtBottom(el);
|
|
2359
2830
|
var upward = el.scrollTop < lastScrollTopRef.current - 2;
|
|
@@ -2362,6 +2833,20 @@ window.__ModuleLoader__.load({
|
|
|
2362
2833
|
if (hoverCard) hoverEnd();
|
|
2363
2834
|
}
|
|
2364
2835
|
lastScrollTopRef.current = el.scrollTop;
|
|
2836
|
+
syncBackBtn(el); // reached the remembered place by hand? the button is done
|
|
2837
|
+
// Remember what the reader is looking at. Sampling on every scroll event is what
|
|
2838
|
+
// lets a change the reader did NOT start — the host extending the window by itself
|
|
2839
|
+
// — be compensated too: the layout effect knows where the anchored row was before
|
|
2840
|
+
// the height changed and puts it back before the push is ever painted.
|
|
2841
|
+
sampleAnchor(el);
|
|
2842
|
+
// The gap is filled AHEAD of the reader, so "reached the very top" is not the only
|
|
2843
|
+
// trigger any more: on any upward movement, ask the auto-loader whether the
|
|
2844
|
+
// unloaded stretch is coming into range (it decides, see frontierApproaching).
|
|
2845
|
+
// Throttled, because that probe reads geometry.
|
|
2846
|
+
if (upward && el.scrollTop > 24 && Date.now() - autoProbeRef.current > 120) {
|
|
2847
|
+
autoProbeRef.current = Date.now();
|
|
2848
|
+
autoLoadStep(el);
|
|
2849
|
+
}
|
|
2365
2850
|
if (el.scrollTop <= 24) {
|
|
2366
2851
|
// Reaching the top is the ONLY place the exhausted case can be told
|
|
2367
2852
|
// apart from "keep scrolling": after a search the list is parked at the
|
|
@@ -2370,8 +2855,10 @@ window.__ModuleLoader__.load({
|
|
|
2370
2855
|
if (!progressed && upward) {
|
|
2371
2856
|
if (query.trim() !== "") {
|
|
2372
2857
|
if (hint !== "oldest") setHint("oldest");
|
|
2373
|
-
} else {
|
|
2858
|
+
} else if (atHistoryStart()) {
|
|
2374
2859
|
showBanner(T("search.hintOldest"));
|
|
2860
|
+
} else if (!prefs.autoLoad) {
|
|
2861
|
+
showBanner(T("hint.autoOff"));
|
|
2375
2862
|
}
|
|
2376
2863
|
}
|
|
2377
2864
|
}
|
|
@@ -2610,6 +3097,11 @@ window.__ModuleLoader__.load({
|
|
|
2610
3097
|
// render (the "did that turn actually page in?" verdict below).
|
|
2611
3098
|
var groupsLiveRef = react.useRef(groups);
|
|
2612
3099
|
groupsLiveRef.current = groups;
|
|
3100
|
+
react.useEffect(function () {
|
|
3101
|
+
return function () {
|
|
3102
|
+
if (keepPlaceRafRef.current) cancelAnimationFrame(keepPlaceRafRef.current);
|
|
3103
|
+
};
|
|
3104
|
+
}, []);
|
|
2613
3105
|
// One history-rebuild retry per session after a jump that never landed.
|
|
2614
3106
|
// Hooks must sit ABOVE every early return (this panel returns null for an
|
|
2615
3107
|
// empty conversation) or React aborts with #310.
|
|
@@ -2816,6 +3308,25 @@ window.__ModuleLoader__.load({
|
|
|
2816
3308
|
var followFrame = null;
|
|
2817
3309
|
var followTimer = null;
|
|
2818
3310
|
var lastRun = 0;
|
|
3311
|
+
// A long session's conversation holds thousands of message anchors, and reading a
|
|
3312
|
+
// rect for every one of them ~16x/s (plus the 250ms poll) is main-thread work that
|
|
3313
|
+
// GROWS with the session length — that is frame loss the reader notices after
|
|
3314
|
+
// leaving the app open for a long time. So the node list is cached and the scan
|
|
3315
|
+
// starts at the boundary seen last time: a handful of rects per update instead of
|
|
3316
|
+
// thousands. The cache is dropped when the port changes or its two ends change
|
|
3317
|
+
// (the host mounts and unmounts messages while it repages the window).
|
|
3318
|
+
var cachedSp = null, cachedRows = null, cachedCount = -1, cachedFirst = null, cachedLast = null;
|
|
3319
|
+
var cursor = 0;
|
|
3320
|
+
var rowsOf = function (sp) {
|
|
3321
|
+
var list = sp.querySelectorAll("[data-chat-anchor-key]");
|
|
3322
|
+
var first = list.length > 0 ? list[0].dataset.chatAnchorKey : null;
|
|
3323
|
+
var last = list.length > 0 ? list[list.length - 1].dataset.chatAnchorKey : null;
|
|
3324
|
+
if (sp !== cachedSp || list.length !== cachedCount || first !== cachedFirst || last !== cachedLast) {
|
|
3325
|
+
cachedSp = sp; cachedRows = list; cachedCount = list.length;
|
|
3326
|
+
cachedFirst = first; cachedLast = last; cursor = 0;
|
|
3327
|
+
}
|
|
3328
|
+
return cachedRows;
|
|
3329
|
+
};
|
|
2819
3330
|
var update = function (force) {
|
|
2820
3331
|
// a jump glide carries the conversation past many turns; following each
|
|
2821
3332
|
// transient position would make the rail sprint through the whole
|
|
@@ -2824,9 +3335,13 @@ window.__ModuleLoader__.load({
|
|
|
2824
3335
|
if (glide) return;
|
|
2825
3336
|
// scroll frames are hot and a document-level capture listener sees every
|
|
2826
3337
|
// scroll in the app; the rail only changes when a different turn crosses
|
|
2827
|
-
// the viewport, so cap the row measurement
|
|
3338
|
+
// the viewport, so cap the row measurement. While the panel is COLLAPSED
|
|
3339
|
+
// (or handed to the curtain) nothing on screen can change, so the cap goes
|
|
3340
|
+
// way up — but the scan still runs, because it is also what remembers the
|
|
3341
|
+
// reader's position.
|
|
2828
3342
|
var now = Date.now();
|
|
2829
|
-
|
|
3343
|
+
var gap = panelVisibleRef.current ? 60 : 800;
|
|
3344
|
+
if (!force && now - lastRun < gap) return;
|
|
2830
3345
|
lastRun = now;
|
|
2831
3346
|
if (!chatViewRef.current) return;
|
|
2832
3347
|
var sp = document.querySelector("[data-conversation-scroll]");
|
|
@@ -2834,16 +3349,25 @@ window.__ModuleLoader__.load({
|
|
|
2834
3349
|
var lr = sp.getBoundingClientRect();
|
|
2835
3350
|
var vTop = lr.top;
|
|
2836
3351
|
var vBottom = lr.top + lr.height;
|
|
2837
|
-
var rows = sp
|
|
3352
|
+
var rows = rowsOf(sp);
|
|
3353
|
+
if (!rows || rows.length === 0) return;
|
|
3354
|
+
if (cursor > rows.length - 1) cursor = rows.length - 1;
|
|
3355
|
+
if (cursor < 0) cursor = 0;
|
|
3356
|
+
var cr = rows[cursor].getBoundingClientRect();
|
|
3357
|
+
while (cr.bottom <= vTop && cursor + 1 < rows.length) { cursor++; cr = rows[cursor].getBoundingClientRect(); }
|
|
3358
|
+
while (cr.bottom > vTop && cursor > 0) {
|
|
3359
|
+
var pr = rows[cursor - 1].getBoundingClientRect();
|
|
3360
|
+
if (pr.bottom <= vTop) break;
|
|
3361
|
+
cursor--; cr = pr;
|
|
3362
|
+
}
|
|
2838
3363
|
var actives = [];
|
|
2839
3364
|
var seen = {};
|
|
2840
|
-
for (var i =
|
|
3365
|
+
for (var i = cursor; i < rows.length; i++) {
|
|
2841
3366
|
var r = rows[i].getBoundingClientRect();
|
|
2842
|
-
if (r.top
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
}
|
|
3367
|
+
if (r.top >= vBottom) break; // document order: nothing below is visible either
|
|
3368
|
+
var k = rows[i].dataset.chatAnchorKey;
|
|
3369
|
+
var gi = keyToGroup[k];
|
|
3370
|
+
if (gi !== undefined && !seen[gi]) { seen[gi] = true; actives.push(gi); }
|
|
2847
3371
|
}
|
|
2848
3372
|
var sig = actives.slice().sort().join(",");
|
|
2849
3373
|
if (sig === activeSigRef.current) return;
|
|
@@ -2869,11 +3393,12 @@ window.__ModuleLoader__.load({
|
|
|
2869
3393
|
if (!el) return;
|
|
2870
3394
|
var node = el.querySelector('[data-group-idx="' + gi0 + '"]');
|
|
2871
3395
|
if (!node) return;
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
3396
|
+
// The turn being read goes to the TOP of the list. This used to CENTRE it, which is
|
|
3397
|
+
// why the follow stopped "a little short of the bottom": for the newest turn the
|
|
3398
|
+
// centre is above the end of the list, so the scroll landed half a viewport early.
|
|
3399
|
+
// Aligning to the top needs no clamp at all — the list's own end does the work.
|
|
3400
|
+
var delta = node.getBoundingClientRect().top - el.getBoundingClientRect().top;
|
|
3401
|
+
if (Math.abs(delta) > 1) el.scrollTop = el.scrollTop + delta;
|
|
2877
3402
|
};
|
|
2878
3403
|
followFrame = requestAnimationFrame(function () { followFrame = requestAnimationFrame(place); });
|
|
2879
3404
|
followTimer = setTimeout(place, 90); // the row may only exist after the expansion commits
|
|
@@ -3187,7 +3712,14 @@ window.__ModuleLoader__.load({
|
|
|
3187
3712
|
var posTimerRef = react.useRef(null);
|
|
3188
3713
|
var rememberRef = react.useRef(true);
|
|
3189
3714
|
rememberRef.current = !!prefs.remember;
|
|
3715
|
+
// the auto-loader's switch, mirrored for the same reason: its retry fires from an
|
|
3716
|
+
// older render's closure, and turning the setting off must stop it immediately
|
|
3717
|
+
autoLoadRef.current = !!prefs.autoLoad;
|
|
3718
|
+
// Is any of the outline actually on screen? The follow scan is cheap now, but while
|
|
3719
|
+
// the panel sits collapsed there is no point measuring at scroll-frame rate.
|
|
3720
|
+
panelVisibleRef.current = !!(open || sheetOpen);
|
|
3190
3721
|
react.useEffect(function () { return function () { if (posTimerRef.current) clearTimeout(posTimerRef.current); }; }, []);
|
|
3722
|
+
react.useEffect(function () { return function () { if (autoTimerRef.current) clearTimeout(autoTimerRef.current); }; }, []);
|
|
3191
3723
|
// `g` is the outline group the reader is looking at (its turn AND its start seq,
|
|
3192
3724
|
// so the position can be paged back in later, not just jumped to while loaded)
|
|
3193
3725
|
var savePosition = function (g) {
|
|
@@ -3290,16 +3822,73 @@ window.__ModuleLoader__.load({
|
|
|
3290
3822
|
openTurn(saved.turn, seq === null ? undefined : seq, true);
|
|
3291
3823
|
}, [groups, prefs.remember, chatViewActive, sessionId]);
|
|
3292
3824
|
|
|
3293
|
-
// ----
|
|
3825
|
+
// ---- holding the reader's place, before the browser paints ----------------
|
|
3826
|
+
// Every render can have changed the list's height: older rows revealed, a ghost
|
|
3827
|
+
// replaced by a whole real subtree, the host extending its window on its own. This
|
|
3828
|
+
// runs after React wrote the DOM and BEFORE the browser paints, which is the only
|
|
3829
|
+
// place the correction is invisible — the reader sees the new rows appear above the
|
|
3830
|
+
// viewport instead of seeing their own content pushed down and then snapped back.
|
|
3831
|
+
// It writes nothing when the anchor has not moved, so an idle list costs one
|
|
3832
|
+
// measurement per render and nothing else.
|
|
3833
|
+
// No dep array on purpose: the height can change for reasons this component does not
|
|
3834
|
+
// get told about (the host repaging its window changes the rows' heights without
|
|
3835
|
+
// necessarily changing the group list's identity), and the check is cheap — one
|
|
3836
|
+
// attribute query and one rect, and it writes nothing when the anchor has not moved.
|
|
3837
|
+
react.useLayoutEffect(function () {
|
|
3838
|
+
var el = listRef.current;
|
|
3839
|
+
if (el) restoreAnchor(el);
|
|
3840
|
+
});
|
|
3841
|
+
|
|
3842
|
+
// ---- opening a surface: land on the turn being read ----------------------
|
|
3843
|
+
// ONE rule for both surfaces: what the reader is reading sits at the TOP of the list —
|
|
3844
|
+
// except when it is one of the newest turns, where the list cannot scroll any further and
|
|
3845
|
+
// the row therefore lands at the bottom. Nothing clamps it: the rule limits itself.
|
|
3846
|
+
// The docked panel used to just show wherever the list had last been left, which is
|
|
3847
|
+
// exactly why the two surfaces disagreed about where they open.
|
|
3848
|
+
//
|
|
3294
3849
|
// Hooks stay above the early return; the helpers they call are assigned later
|
|
3295
3850
|
// in the render body, which is fine because this runs after the render.
|
|
3296
|
-
|
|
3297
|
-
if (!sheetOpen) return;
|
|
3851
|
+
var landOnReadingRow = function () {
|
|
3298
3852
|
var el = listRef.current;
|
|
3299
|
-
if (el
|
|
3853
|
+
if (!el) return undefined;
|
|
3854
|
+
// A reader who had not scrolled the outline is at its bottom, and opening the CURTAIN
|
|
3855
|
+
// must not move them: its container geometry changes wholesale, and the "hold the
|
|
3856
|
+
// anchor" correction would put a stale row back where it was — the list then sat a
|
|
3857
|
+
// little above the newest turn until the follow walked it down. Pin the bottom instead,
|
|
3858
|
+
// for this transition only, and skip the reveal (there is nothing to reveal to: they
|
|
3859
|
+
// are already reading the newest turn). The docked panel reshapes nothing — it opens
|
|
3860
|
+
// around a list that kept its geometry — so there it has nothing to pin.
|
|
3861
|
+
if (atBottomRef.current) {
|
|
3862
|
+
if (!sheetOpen) return undefined;
|
|
3863
|
+
anchorRef.current = null; // no compensation against a stale anchor
|
|
3864
|
+
el.scrollTop = el.scrollHeight;
|
|
3865
|
+
syncAtBottom(el);
|
|
3866
|
+
var again = requestAnimationFrame(function () { // ...and once more, after layout
|
|
3867
|
+
var el2 = listRef.current;
|
|
3868
|
+
if (!el2 || !atBottomRef.current) return;
|
|
3869
|
+
el2.scrollTop = el2.scrollHeight;
|
|
3870
|
+
syncAtBottom(el2);
|
|
3871
|
+
sampleAnchor(el2);
|
|
3872
|
+
});
|
|
3873
|
+
return function () { cancelAnimationFrame(again); };
|
|
3874
|
+
}
|
|
3300
3875
|
var gi = activeGroup && activeGroup.length > 0 ? activeGroup[0] : -1;
|
|
3301
3876
|
if (gi >= 0) revealGroupInOutline(gi);
|
|
3877
|
+
return undefined;
|
|
3878
|
+
};
|
|
3879
|
+
react.useEffect(function () {
|
|
3880
|
+
if (!sheetOpen) return undefined;
|
|
3881
|
+
var el = listRef.current;
|
|
3882
|
+
if (el && typeof el.focus === "function") el.focus();
|
|
3883
|
+
return landOnReadingRow();
|
|
3302
3884
|
}, [sheetOpen]);
|
|
3885
|
+
// The docked panel obeys the same rule when the reader opens IT. Only its own open counts
|
|
3886
|
+
// (`open` false -> true): the curtain handing the panel back leaves `open` true, and there
|
|
3887
|
+
// the list keeps the place the reader was just looking at in the curtain.
|
|
3888
|
+
react.useEffect(function () {
|
|
3889
|
+
if (!open) return undefined;
|
|
3890
|
+
return landOnReadingRow();
|
|
3891
|
+
}, [open]);
|
|
3303
3892
|
|
|
3304
3893
|
// ---- nothing to show without headings ----
|
|
3305
3894
|
if (groups.length === 0) return null;
|
|
@@ -3323,22 +3912,25 @@ window.__ModuleLoader__.load({
|
|
|
3323
3912
|
// rendered at its COLLAPSED geometry, so the same left/clip transition that
|
|
3324
3913
|
// serves open/collapse also serves the handoff: no teleport, no flash.
|
|
3325
3914
|
var geoOpen = open && !panelAside;
|
|
3326
|
-
|
|
3915
|
+
// The box is always PARKED at its collapsed spot and the open state is a transform
|
|
3916
|
+
// offset on top of it (see panelTransition): same visual geometry as before, but the
|
|
3917
|
+
// slide is compositable. `openLeft` is the clamped open position, `parkedLeft` the
|
|
3918
|
+
// off-area spot the collapsed panel occupies.
|
|
3919
|
+
var openLeft, parkedLeft;
|
|
3327
3920
|
if (dockRight) {
|
|
3328
3921
|
// RIGHT dock: keep the panel's VISIBLE right edge pinned at the SAME
|
|
3329
3922
|
// line when collapsed as when expanded (the expand edge is correct).
|
|
3330
|
-
// open: left = openRight, visible right edge = openRight+panelW
|
|
3331
|
-
// closed: left = openRight + panelW, clip-right panelW hides it fully,
|
|
3332
|
-
// so the visible right edge stays at openRight+panelW -> the
|
|
3333
|
-
// collapse shrinks toward that line and vanishes exactly there.
|
|
3334
3923
|
var openRight = window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW;
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
// LEFT dock open (left dock is the confirmed-correct reference — untouched)
|
|
3338
|
-
panelLeft = clampLeft(viewport ? viewport.left + 8 : 8);
|
|
3924
|
+
openLeft = clampLeft(openRight);
|
|
3925
|
+
parkedLeft = openRight + panelW;
|
|
3339
3926
|
} else {
|
|
3340
|
-
|
|
3927
|
+
// LEFT dock open (left dock is the confirmed-correct reference — untouched)
|
|
3928
|
+
openLeft = clampLeft(viewport ? viewport.left + 8 : 8);
|
|
3929
|
+
parkedLeft = (viewport ? viewport.left - panelW - 8 : -(panelW + 48));
|
|
3341
3930
|
}
|
|
3931
|
+
var panelLeft = parkedLeft;
|
|
3932
|
+
var panelOffset = geoOpen ? (openLeft - parkedLeft) : 0;
|
|
3933
|
+
var panelTransform = panelOffset === 0 ? "none" : "translateX(" + zLen(panelOffset) + ")";
|
|
3342
3934
|
var vMaxH = viewport ? Math.max(200, viewport.height - 28) : "72vh";
|
|
3343
3935
|
// same reasoning vertically: the draggable offset is stored in localStorage
|
|
3344
3936
|
// and can outlive the layout it was chosen for
|
|
@@ -3762,16 +4354,34 @@ window.__ModuleLoader__.load({
|
|
|
3762
4354
|
|
|
3763
4355
|
// reveal one group inside the outline list (shared by Enter-stepping and by
|
|
3764
4356
|
// jumps into a turn that had to be loaded first)
|
|
4357
|
+
// Put a group's row at the TOP of the list, instantly. The previous way —
|
|
4358
|
+
// `scrollIntoView({ behavior: "smooth", block: "nearest" })` — only scrolled the minimum
|
|
4359
|
+
// distance (so the row could end up anywhere in the viewport, which reads as "the jump is
|
|
4360
|
+
// not accurate") and it ANIMATED there, which is the scroll the reader saw when opening
|
|
4361
|
+
// the panel or the curtain.
|
|
4362
|
+
//
|
|
4363
|
+
// Aligning to the top is also self-limiting, which is why it needs no special case: on the
|
|
4364
|
+
// newest turn the list simply cannot scroll further, so `scrollTop` clamps at the end and
|
|
4365
|
+
// that row lands exactly at the bottom.
|
|
4366
|
+
var alignGroupToTop = function (gi) {
|
|
4367
|
+
var el = listRef.current;
|
|
4368
|
+
if (!el) return false;
|
|
4369
|
+
var node = el.querySelector('[data-group-idx="' + gi + '"]');
|
|
4370
|
+
if (!node) return false;
|
|
4371
|
+
var delta = node.getBoundingClientRect().top - el.getBoundingClientRect().top;
|
|
4372
|
+
if (Math.abs(delta) > 0.5) el.scrollTop = el.scrollTop + delta;
|
|
4373
|
+
return true;
|
|
4374
|
+
};
|
|
3765
4375
|
var revealGroupInOutline = function (gi) {
|
|
3766
4376
|
setVisibleCount(function (prev) {
|
|
3767
4377
|
return Math.max(prev, Math.min(groups.length, groups.length - gi));
|
|
3768
4378
|
});
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
}
|
|
4379
|
+
// the row may only exist once the commit has happened, so retry across the next frames
|
|
4380
|
+
alignGroupToTop(gi);
|
|
4381
|
+
requestAnimationFrame(function () {
|
|
4382
|
+
alignGroupToTop(gi);
|
|
4383
|
+
requestAnimationFrame(function () { alignGroupToTop(gi); });
|
|
4384
|
+
});
|
|
3775
4385
|
};
|
|
3776
4386
|
|
|
3777
4387
|
// jump to the n-th match, cycling; also reveal the group in the outline.
|
|
@@ -3996,6 +4606,10 @@ window.__ModuleLoader__.load({
|
|
|
3996
4606
|
if (!open) return;
|
|
3997
4607
|
swallow(e);
|
|
3998
4608
|
setNavIdx(-1);
|
|
4609
|
+
// Esc is a way of LEAVING the docked panel, so it records the same thing the ✕ does.
|
|
4610
|
+
// Without this the memory kept whatever the last ✕ close had stored, and reopening
|
|
4611
|
+
// would offer to go back to a row the reader had left two closes ago.
|
|
4612
|
+
noteClosed("dock");
|
|
3999
4613
|
setOpen(false);
|
|
4000
4614
|
}
|
|
4001
4615
|
};
|
|
@@ -4050,7 +4664,14 @@ window.__ModuleLoader__.load({
|
|
|
4050
4664
|
// travelling when the curtain took over, which looked like it flashed away.
|
|
4051
4665
|
var panelAway = !open || panelAside === "tuck";
|
|
4052
4666
|
var slide = panelAway ? "0.28s " + EASE : "0.6s " + EASE;
|
|
4053
|
-
|
|
4667
|
+
// The slide is a TRANSFORM, not `left`. Animating `left` re-lays out the whole panel —
|
|
4668
|
+
// header, toolbar and every rendered outline row — on every frame of the animation, and
|
|
4669
|
+
// once a long session has loaded thousands of rows that layout can no longer keep up
|
|
4670
|
+
// with the (compositable) `clip-path` wipe next to it: the wipe ran on while the slide
|
|
4671
|
+
// stalled, which is the "a line sweeps across and the panel never really slides" the
|
|
4672
|
+
// reader reported. A transform is handed to the compositor, so the animation no longer
|
|
4673
|
+
// depends on how big the list is.
|
|
4674
|
+
var panelTransition = (zoomJustChanged ? "" : "transform " + slide + ", ")
|
|
4054
4675
|
+ "clip-path " + slide + ", opacity " + (panelAway ? "0.14s ease 0.26s" : "0.45s ease");
|
|
4055
4676
|
|
|
4056
4677
|
// clip the panel at the dock edge while collapsed, so sliding away looks
|
|
@@ -4142,7 +4763,7 @@ window.__ModuleLoader__.load({
|
|
|
4142
4763
|
viewBox: "0 0 14 14",
|
|
4143
4764
|
fill: "none",
|
|
4144
4765
|
stroke: "currentColor",
|
|
4145
|
-
strokeWidth:
|
|
4766
|
+
strokeWidth: 2.2,
|
|
4146
4767
|
strokeLinecap: "round",
|
|
4147
4768
|
style: { display: "block" },
|
|
4148
4769
|
children: [
|
|
@@ -4163,13 +4784,18 @@ window.__ModuleLoader__.load({
|
|
|
4163
4784
|
viewBox: "0 0 14 14",
|
|
4164
4785
|
fill: "none",
|
|
4165
4786
|
stroke: "currentColor",
|
|
4166
|
-
strokeWidth:
|
|
4787
|
+
strokeWidth: 2.2,
|
|
4167
4788
|
strokeLinecap: "round",
|
|
4168
4789
|
strokeLinejoin: "round",
|
|
4169
4790
|
style: { display: "block" },
|
|
4170
4791
|
children: [
|
|
4171
|
-
|
|
4172
|
-
|
|
4792
|
+
// A slightly LARGER bubble (11.4 x 7.7), and the whole glyph sits a touch BELOW the
|
|
4793
|
+
// box centre on purpose: the bubble body is the visual mass while the tail hangs
|
|
4794
|
+
// off the bottom-left, so a geometrically centred bubble reads as top-heavy. The
|
|
4795
|
+
// stroke bbox is 1.4..13.8 (centre 7.6, i.e. 0.6 low) and the horizontal centre is
|
|
4796
|
+
// still exactly 7.
|
|
4797
|
+
react_jsx_runtime.jsx("rect", { x: 1.3, y: 2.5, width: 11.4, height: 7.7, rx: 1.7 }),
|
|
4798
|
+
react_jsx_runtime.jsx("path", { d: "M4.5 10.2v2.5l2.9-2.5" })
|
|
4173
4799
|
]
|
|
4174
4800
|
}),
|
|
4175
4801
|
{ "data-questions": questionsOnly ? "on" : "off" }
|
|
@@ -4184,14 +4810,16 @@ window.__ModuleLoader__.load({
|
|
|
4184
4810
|
viewBox: "0 0 24 24",
|
|
4185
4811
|
// overflow visible: the stroke may extend past the
|
|
4186
4812
|
// viewBox without being clipped at the svg boundary
|
|
4187
|
-
style: { display: "block",
|
|
4813
|
+
style: { display: "block", overflow: "visible" },
|
|
4188
4814
|
fill: "none",
|
|
4189
4815
|
stroke: "currentColor",
|
|
4190
4816
|
strokeWidth: 3.6,
|
|
4191
4817
|
strokeLinecap: "round",
|
|
4192
4818
|
children: [
|
|
4193
|
-
|
|
4194
|
-
|
|
4819
|
+
// centred by geometry (bbox 3.5..20.5, centre 12,12) rather than by a nudge:
|
|
4820
|
+
// a fractional translate leaves the glyph a half pixel off in a round button
|
|
4821
|
+
react_jsx_runtime.jsx("circle", { cx: 10.5, cy: 10.5, r: 7 }),
|
|
4822
|
+
react_jsx_runtime.jsx("line", { x1: 20.5, y1: 20.5, x2: 15.5, y2: 15.5 })
|
|
4195
4823
|
]
|
|
4196
4824
|
})
|
|
4197
4825
|
);
|
|
@@ -4202,13 +4830,14 @@ window.__ModuleLoader__.load({
|
|
|
4202
4830
|
width: tbIconLg,
|
|
4203
4831
|
height: tbIconLg,
|
|
4204
4832
|
viewBox: "0 0 24 24",
|
|
4205
|
-
style: { display: "block",
|
|
4833
|
+
style: { display: "block", overflow: "visible" },
|
|
4206
4834
|
fill: "none",
|
|
4207
4835
|
stroke: "currentColor",
|
|
4208
4836
|
strokeWidth: 3.4,
|
|
4209
4837
|
strokeLinecap: "round",
|
|
4210
4838
|
children: [
|
|
4211
|
-
// cross lines span 5..19, intersection (12,12) centered
|
|
4839
|
+
// cross lines span 5..19, intersection (12,12) centered — no nudge: a translate
|
|
4840
|
+
// here is what left the reader's ✕ visibly right of the round button's centre
|
|
4212
4841
|
react_jsx_runtime.jsx("line", { x1: 5, y1: 5, x2: 19, y2: 19 }),
|
|
4213
4842
|
react_jsx_runtime.jsx("line", { x1: 19, y1: 5, x2: 5, y2: 19 })
|
|
4214
4843
|
]
|
|
@@ -4279,7 +4908,7 @@ window.__ModuleLoader__.load({
|
|
|
4279
4908
|
var levelsPopSheetEl = (sheetOpen && (levelsOpen || levelsClosing)) ? buildLevelsPop(true) : null;
|
|
4280
4909
|
// dock toggle: the triangle tips toward the side it will move TO
|
|
4281
4910
|
var dockBtnEl = iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 });
|
|
4282
|
-
// the curtain button: a surface with
|
|
4911
|
+
// the curtain button: a surface with the arrow dropping out of its TOP edge
|
|
4283
4912
|
var sheetBtnEl = react_jsx_runtime.jsx("button", {
|
|
4284
4913
|
className: "dqt-sheet-btn",
|
|
4285
4914
|
onClick: openSheet,
|
|
@@ -4317,16 +4946,22 @@ window.__ModuleLoader__.load({
|
|
|
4317
4946
|
strokeWidth: 1.8,
|
|
4318
4947
|
strokeLinecap: "round",
|
|
4319
4948
|
strokeLinejoin: "round",
|
|
4320
|
-
style: { display: "block" },
|
|
4949
|
+
style: { display: "block", overflow: "visible" },
|
|
4321
4950
|
children: [
|
|
4322
|
-
|
|
4323
|
-
|
|
4951
|
+
// A landscape surface 12.0 x 9.0 (centred in the 14 box: 1.0..13.0 / 2.5..11.5) —
|
|
4952
|
+
// neither the portrait slab nor the flat strip the earlier attempts were. The
|
|
4953
|
+
// rectangle itself carries the SAME border weight as the questions bubble (2.2,
|
|
4954
|
+
// via its own strokeWidth) while the arrow keeps the lighter inherited 1.8, so the
|
|
4955
|
+
// frame reads as the frame and the arrow stays a mark inside it. `overflow:
|
|
4956
|
+
// visible` because the 2.2 border on a 12-wide rect reaches x = -0.1.
|
|
4957
|
+
react_jsx_runtime.jsx("rect", { x: 1.0, y: 2.5, width: 12.0, height: 9.0, rx: 1.6, strokeWidth: 2.2 }),
|
|
4958
|
+
react_jsx_runtime.jsx("path", { d: "M7 2.5V9.1M5.2 7.3L7 9.1l1.8-1.8" })
|
|
4324
4959
|
]
|
|
4325
4960
|
})
|
|
4326
4961
|
});
|
|
4327
4962
|
// collapse (docked panel only): thick SVG cross, nudged slightly down
|
|
4328
4963
|
var headerCloseBtnEl = react_jsx_runtime.jsx("button", {
|
|
4329
|
-
onClick: function () { setOpen(false); },
|
|
4964
|
+
onClick: function () { noteClosed("dock"); setOpen(false); },
|
|
4330
4965
|
title: T("handle.collapse"),
|
|
4331
4966
|
style: {
|
|
4332
4967
|
width: 24,
|
|
@@ -4353,13 +4988,13 @@ window.__ModuleLoader__.load({
|
|
|
4353
4988
|
width: 15,
|
|
4354
4989
|
height: 15,
|
|
4355
4990
|
viewBox: "0 0 24 24",
|
|
4356
|
-
style: { display: "block",
|
|
4991
|
+
style: { display: "block", overflow: "visible" },
|
|
4357
4992
|
fill: "none",
|
|
4358
4993
|
stroke: "currentColor",
|
|
4359
4994
|
strokeWidth: 3.4,
|
|
4360
4995
|
strokeLinecap: "round",
|
|
4361
4996
|
children: [
|
|
4362
|
-
// cross lines span 5..19, intersection (12,12) centered
|
|
4997
|
+
// cross lines span 5..19, intersection (12,12) centered (no nudge here either)
|
|
4363
4998
|
react_jsx_runtime.jsx("line", { x1: 5, y1: 5, x2: 19, y2: 19 }),
|
|
4364
4999
|
react_jsx_runtime.jsx("line", { x1: 19, y1: 5, x2: 5, y2: 19 })
|
|
4365
5000
|
]
|
|
@@ -4699,6 +5334,12 @@ window.__ModuleLoader__.load({
|
|
|
4699
5334
|
"aria-label": T("a11y.list"),
|
|
4700
5335
|
style: {
|
|
4701
5336
|
overflowY: "auto",
|
|
5337
|
+
// We are the only thing that holds the reader's place in this list (see the
|
|
5338
|
+
// paging block): the browser's own scroll anchoring would also adjust
|
|
5339
|
+
// `scrollTop` when a ghost turns into a real subtree, and two compensations for
|
|
5340
|
+
// one growth fight each other — the browser's is invisible to our measurement,
|
|
5341
|
+
// so our correction would then overshoot and the content would jump UP.
|
|
5342
|
+
overflowAnchor: "none",
|
|
4702
5343
|
// NO top padding: a sticky child is confined to its containing
|
|
4703
5344
|
// block, so a padded scroll container would hold the pinned group
|
|
4704
5345
|
// header that many pixels below the toolbar (a visible gap). Spacing
|
|
@@ -4762,6 +5403,10 @@ window.__ModuleLoader__.load({
|
|
|
4762
5403
|
]
|
|
4763
5404
|
})
|
|
4764
5405
|
}) : null;
|
|
5406
|
+
// Whether the one-shot "back to where I left this surface" button is on: measured here,
|
|
5407
|
+
// after every piece of this pass's chrome geometry is known, and against the memory of
|
|
5408
|
+
// the surface that is actually on screen (see the restore block near the top).
|
|
5409
|
+
var showBack = awayFromRestore(listRef.current);
|
|
4765
5410
|
// the outline and the search column share one row in BOTH layouts, so the list
|
|
4766
5411
|
// is never moved between parents when the curtain opens or closes
|
|
4767
5412
|
var bodyRowEl = react_jsx_runtime.jsx("div", {
|
|
@@ -4819,6 +5464,7 @@ window.__ModuleLoader__.load({
|
|
|
4819
5464
|
color: C.text,
|
|
4820
5465
|
opacity: panelOpacity,
|
|
4821
5466
|
clipPath: panelClip,
|
|
5467
|
+
transform: panelTransform,
|
|
4822
5468
|
// inner shadow only (no outer): theme-aware (white bevel in dark mode,
|
|
4823
5469
|
// soft shade in light) — outer shadow would be clipped by the collapse
|
|
4824
5470
|
// clip-path and the panel edges.
|
|
@@ -4899,8 +5545,10 @@ window.__ModuleLoader__.load({
|
|
|
4899
5545
|
// curtain it also clears the pushed-in search column
|
|
4900
5546
|
right: (sheetOpen && sheetColW > 0 ? SHEET_SEARCH_W : 0) + 18,
|
|
4901
5547
|
bottom: 12,
|
|
4902
|
-
|
|
4903
|
-
|
|
5548
|
+
// the curtain scales up every other control (24 -> 32); this floating button
|
|
5549
|
+
// was the one left behind at the docked size
|
|
5550
|
+
width: sheetOpen ? 34 : 26,
|
|
5551
|
+
height: sheetOpen ? 34 : 26,
|
|
4904
5552
|
display: "flex",
|
|
4905
5553
|
alignItems: "center",
|
|
4906
5554
|
justifyContent: "center",
|
|
@@ -4924,8 +5572,8 @@ window.__ModuleLoader__.load({
|
|
|
4924
5572
|
transition: "opacity 0.22s ease, transform 0.22s ease, background 0.18s ease, color 0.18s ease, border-color 0.18s ease"
|
|
4925
5573
|
},
|
|
4926
5574
|
children: react_jsx_runtime.jsx("svg", {
|
|
4927
|
-
width: 14,
|
|
4928
|
-
height: 14,
|
|
5575
|
+
width: sheetOpen ? 18 : 14,
|
|
5576
|
+
height: sheetOpen ? 18 : 14,
|
|
4929
5577
|
viewBox: "0 0 14 14",
|
|
4930
5578
|
fill: "none",
|
|
4931
5579
|
stroke: "currentColor",
|
|
@@ -4939,6 +5587,67 @@ window.__ModuleLoader__.load({
|
|
|
4939
5587
|
]
|
|
4940
5588
|
})
|
|
4941
5589
|
}),
|
|
5590
|
+
// "back to the place I scrolled to": the mirror of the button above, in the
|
|
5591
|
+
// list's upper-right corner, arrow pointing UP. It appears only while the list is
|
|
5592
|
+
// far from the position the reader last reached by wheel/key — the follow and the
|
|
5593
|
+
// curtain handoff both move the outline for the reader, and until now there was no
|
|
5594
|
+
// way back to where they had been browsing. Same surface vocabulary and same
|
|
5595
|
+
// curtain scaling as the bottom button.
|
|
5596
|
+
react_jsx_runtime.jsx("button", {
|
|
5597
|
+
onClick: backToBrowse,
|
|
5598
|
+
className: "dqt-top-btn",
|
|
5599
|
+
"data-at-mine": showBack ? "off" : "on",
|
|
5600
|
+
title: T("outline.backToMine"),
|
|
5601
|
+
tabIndex: showBack ? 0 : -1,
|
|
5602
|
+
"aria-hidden": showBack ? undefined : "true",
|
|
5603
|
+
onMouseEnter: function (e) {
|
|
5604
|
+
e.currentTarget.style.color = C.accent;
|
|
5605
|
+
e.currentTarget.style.borderColor = C.groupEdgeSoft;
|
|
5606
|
+
},
|
|
5607
|
+
onMouseLeave: function (e) {
|
|
5608
|
+
e.currentTarget.style.color = C.text;
|
|
5609
|
+
e.currentTarget.style.borderColor = C.panelBorder;
|
|
5610
|
+
},
|
|
5611
|
+
style: {
|
|
5612
|
+
position: "absolute",
|
|
5613
|
+
right: (sheetOpen && sheetColW > 0 ? SHEET_SEARCH_W : 0) + 18,
|
|
5614
|
+
// below whatever chrome this surface has (measured, see listTop)
|
|
5615
|
+
top: listTop + 12,
|
|
5616
|
+
width: sheetOpen ? 34 : 26,
|
|
5617
|
+
height: sheetOpen ? 34 : 26,
|
|
5618
|
+
display: "flex",
|
|
5619
|
+
alignItems: "center",
|
|
5620
|
+
justifyContent: "center",
|
|
5621
|
+
padding: 0,
|
|
5622
|
+
background: "var(--dsw-specific-menu, rgba(44, 49, 60, 0.99))",
|
|
5623
|
+
border: "1px solid " + C.panelBorder,
|
|
5624
|
+
borderRadius: "50%",
|
|
5625
|
+
cornerShape: "round",
|
|
5626
|
+
color: C.text,
|
|
5627
|
+
cursor: "pointer",
|
|
5628
|
+
zIndex: 6,
|
|
5629
|
+
boxShadow: "0 3px 10px rgba(0, 0, 0, 0.30)",
|
|
5630
|
+
opacity: showBack ? 1 : 0,
|
|
5631
|
+
transform: showBack ? "translateY(0)" : "translateY(-6px)",
|
|
5632
|
+
pointerEvents: showBack ? "auto" : "none",
|
|
5633
|
+
transition: "opacity 0.22s ease, transform 0.22s ease, background 0.18s ease, color 0.18s ease, border-color 0.18s ease"
|
|
5634
|
+
},
|
|
5635
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
5636
|
+
width: sheetOpen ? 18 : 14,
|
|
5637
|
+
height: sheetOpen ? 18 : 14,
|
|
5638
|
+
viewBox: "0 0 14 14",
|
|
5639
|
+
fill: "none",
|
|
5640
|
+
stroke: "currentColor",
|
|
5641
|
+
strokeWidth: 1.8,
|
|
5642
|
+
strokeLinecap: "round",
|
|
5643
|
+
strokeLinejoin: "round",
|
|
5644
|
+
style: { display: "block" },
|
|
5645
|
+
children: [
|
|
5646
|
+
react_jsx_runtime.jsx("polyline", { points: "3.5,9.5 7,6 10.5,9.5" }),
|
|
5647
|
+
react_jsx_runtime.jsx("line", { x1: 3.5, y1: 3, x2: 10.5, y2: 3 })
|
|
5648
|
+
]
|
|
5649
|
+
})
|
|
5650
|
+
}),
|
|
4942
5651
|
toastEl,
|
|
4943
5652
|
// Polite live region: match stepping, result counts and cursor moves land
|
|
4944
5653
|
// here. Visually hidden (`.dqt-sr`) — it exists for assistive tech only.
|
|
@@ -5138,7 +5847,14 @@ window.__ModuleLoader__.load({
|
|
|
5138
5847
|
title: T("sheet.openHandle"),
|
|
5139
5848
|
onClick: function () {
|
|
5140
5849
|
var el = sheetHandleRef.current;
|
|
5141
|
-
|
|
5850
|
+
// Pressed FLUSH into the line — `scaleY(0)`, not a small remainder. At 0.12 the
|
|
5851
|
+
// 17px droplet still had ~2px of dome sitting on the strip once the press had
|
|
5852
|
+
// landed, and because the element only leaves the tree a beat later, that
|
|
5853
|
+
// sliver hung there motionless and was then removed in a single frame: the
|
|
5854
|
+
// "it stops, then it is suddenly gone" the reader reported. Collapsed to zero
|
|
5855
|
+
// it sinks out of sight exactly when the press lands, and its hit area goes
|
|
5856
|
+
// with it.
|
|
5857
|
+
if (el && el.parentElement && el.parentElement.style) el.parentElement.style.transform = "scaleY(0)";
|
|
5142
5858
|
setSheetHandleGone(false);
|
|
5143
5859
|
// The press has to be SEEN before the curtain takes the stage. With the panel
|
|
5144
5860
|
// open it already is: the panel's own 300ms tuck comes first. Collapsed, the
|
|
@@ -5898,9 +6614,13 @@ window.__ModuleLoader__.load({
|
|
|
5898
6614
|
// A turn the paged event window has not loaded yet: the host outline still
|
|
5899
6615
|
// names it (number + bounded previews), so the outline covers the whole
|
|
5900
6616
|
// session. Clicking pages that turn in through the session's jump loader.
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
6617
|
+
// What an unloaded group shows: the badge, the prompt, and the host's own response
|
|
6618
|
+
// preview. That preview is budgeted by the host at up to THREE rail-card lines, so it
|
|
6619
|
+
// is rendered at three lines and legibly — clamped to two at 0.65 opacity it read as
|
|
6620
|
+
// "there is nothing here but my own question", which is exactly what the reader
|
|
6621
|
+
// reported.
|
|
6622
|
+
function ghostBody(C, g) {
|
|
6623
|
+
var out = [
|
|
5904
6624
|
react_jsx_runtime.jsx("div", {
|
|
5905
6625
|
style: { display: "flex", alignItems: "center", gap: 5, minWidth: 0 },
|
|
5906
6626
|
children: [
|
|
@@ -5916,11 +6636,16 @@ window.__ModuleLoader__.load({
|
|
|
5916
6636
|
})
|
|
5917
6637
|
];
|
|
5918
6638
|
if (g.preview) {
|
|
5919
|
-
|
|
5920
|
-
style: { fontSize: 10, lineHeight: "
|
|
6639
|
+
out.push(react_jsx_runtime.jsx("div", {
|
|
6640
|
+
style: { fontSize: 10.5, lineHeight: "14px", color: C.muted, opacity: 0.8, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical" },
|
|
5921
6641
|
children: g.preview
|
|
5922
6642
|
}));
|
|
5923
6643
|
}
|
|
6644
|
+
return out;
|
|
6645
|
+
}
|
|
6646
|
+
function renderGhostGroup(g, gi, C, extra) {
|
|
6647
|
+
extra = extra || {};
|
|
6648
|
+
var children = ghostBody(C, g);
|
|
5924
6649
|
return react_jsx_runtime.jsx("div", {
|
|
5925
6650
|
onClick: function () { extra.onOpenTurn(g.turn, g.seq); },
|
|
5926
6651
|
onMouseEnter: function (e) {
|
|
@@ -6256,7 +6981,8 @@ window.__ModuleLoader__.load({
|
|
|
6256
6981
|
field("handle", T("settings.handle"), T("settings.handle.tip"), handleControl, false),
|
|
6257
6982
|
field("fuzzy", T("settings.fuzzy"), "", onoff(prefs.fuzzy, function (v) { commit({ fuzzy: v }); }), true),
|
|
6258
6983
|
field("hover", T("settings.hover"), "", onoff(prefs.hover, function (v) { commit({ hover: v }); }), true),
|
|
6259
|
-
field("remember", T("settings.remember"),
|
|
6984
|
+
field("remember", T("settings.remember"), "", onoff(prefs.remember, function (v) { commit({ remember: v }); }), true),
|
|
6985
|
+
field("autoLoad", T("settings.autoLoad"), "", onoff(prefs.autoLoad, function (v) { commit({ autoLoad: v }); }), true),
|
|
6260
6986
|
field("debug", T("settings.debug"), T("settings.debug.tip"), onoff(prefs.debug, function (v) { commit({ debug: v }); }), true, true)
|
|
6261
6987
|
];
|
|
6262
6988
|
return react_jsx_runtime.jsxs("li", {
|
|
@@ -6461,7 +7187,10 @@ window.__ModuleLoader__.load({
|
|
|
6461
7187
|
// not a detached pill). It grows out of the line when it appears — after a
|
|
6462
7188
|
// retract as much as on first paint — and the click squeezes it flat against
|
|
6463
7189
|
// the line before the curtain takes over (see the element's own transform).
|
|
6464
|
-
|
|
7190
|
+
// Both ends of the pair are flush with the line — 0 -> 1 here, 1 -> 0 on the
|
|
7191
|
+
// press — so the two directions are one motion and its rewind in their
|
|
7192
|
+
// endpoints as well as in their curves.
|
|
7193
|
+
"@keyframes dqt-handle-pop{from{transform:scaleY(0)}to{transform:scaleY(1)}}" +
|
|
6465
7194
|
".dqt-sheet-handle{transform-origin:50% 100%}" +
|
|
6466
7195
|
// coming back it is the press read backwards: FAST OUT OF THE LINE — 74% of the
|
|
6467
7196
|
// easing is done 30ms in — and then the rest settles over the remaining 170ms.
|