dsh-quick-toc 0.1.1 → 0.2.2
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 +466 -6
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -135,6 +135,18 @@ window.__ModuleLoader__.load({
|
|
|
135
135
|
return null;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// count every occurrence of q (case-insensitive) inside text
|
|
139
|
+
function countOccurrences(text, q) {
|
|
140
|
+
var lower = text.toLowerCase();
|
|
141
|
+
var count = 0;
|
|
142
|
+
var idx = 0;
|
|
143
|
+
while ((idx = lower.indexOf(q, idx)) !== -1) {
|
|
144
|
+
count++;
|
|
145
|
+
idx += q.length;
|
|
146
|
+
}
|
|
147
|
+
return count;
|
|
148
|
+
}
|
|
149
|
+
|
|
138
150
|
// find the conversation's "load older messages" button (scoped to the
|
|
139
151
|
// conversation scrollport so panel buttons are never matched)
|
|
140
152
|
function findLoadOlderButton() {
|
|
@@ -148,6 +160,58 @@ window.__ModuleLoader__.load({
|
|
|
148
160
|
return null;
|
|
149
161
|
}
|
|
150
162
|
|
|
163
|
+
// ---- search keyword highlight in the conversation ----
|
|
164
|
+
var highlightSpans = [];
|
|
165
|
+
|
|
166
|
+
function clearHighlights() {
|
|
167
|
+
for (var i = 0; i < highlightSpans.length; i++) {
|
|
168
|
+
var s = highlightSpans[i];
|
|
169
|
+
if (s.parentNode) {
|
|
170
|
+
var t = document.createTextNode(s.textContent);
|
|
171
|
+
s.parentNode.replaceChild(t, s);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
highlightSpans = [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// wrap every occurrence of q (case-insensitive) inside row's text nodes;
|
|
178
|
+
// the occurrence at `currentOcc` gets a distinct "current" highlight
|
|
179
|
+
function highlightRow(row, q, currentOcc) {
|
|
180
|
+
if (!row || !q) return;
|
|
181
|
+
var walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT, null);
|
|
182
|
+
var textNodes = [];
|
|
183
|
+
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
|
184
|
+
var occ = 0;
|
|
185
|
+
for (var i = 0; i < textNodes.length; i++) {
|
|
186
|
+
var node = textNodes[i];
|
|
187
|
+
var text = node.nodeValue;
|
|
188
|
+
if (!text) continue;
|
|
189
|
+
var lower = text.toLowerCase();
|
|
190
|
+
var idx = lower.indexOf(q);
|
|
191
|
+
if (idx < 0) continue;
|
|
192
|
+
var frag = document.createDocumentFragment();
|
|
193
|
+
var before = text.slice(0, idx);
|
|
194
|
+
if (before) frag.appendChild(document.createTextNode(before));
|
|
195
|
+
var mark = document.createElement("span");
|
|
196
|
+
if (occ === currentOcc) {
|
|
197
|
+
mark.className = "dqt-current";
|
|
198
|
+
mark.style.background = "rgba(79,140,255,0.55)";
|
|
199
|
+
mark.style.boxShadow = "0 0 0 1px rgba(79,140,255,0.85)";
|
|
200
|
+
} else {
|
|
201
|
+
mark.style.background = "rgba(79,140,255,0.32)";
|
|
202
|
+
}
|
|
203
|
+
mark.style.borderRadius = "2px";
|
|
204
|
+
mark.style.color = "inherit";
|
|
205
|
+
mark.textContent = text.slice(idx, idx + q.length);
|
|
206
|
+
frag.appendChild(mark);
|
|
207
|
+
highlightSpans.push(mark);
|
|
208
|
+
occ++;
|
|
209
|
+
var after = text.slice(idx + q.length);
|
|
210
|
+
if (after) frag.appendChild(document.createTextNode(after));
|
|
211
|
+
node.parentNode.replaceChild(frag, node);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
151
215
|
// ---------- theme colors: DSH CSS variables (adapt to light/dark) ----------
|
|
152
216
|
var C = {
|
|
153
217
|
panelBg: "var(--dsw-alias-bg-base, rgba(24, 28, 36, 0.96))",
|
|
@@ -322,6 +386,75 @@ window.__ModuleLoader__.load({
|
|
|
322
386
|
var didInitScroll = react.useRef(false);
|
|
323
387
|
var outlineTouchRef = react.useRef(0); // last time the user touched the outline
|
|
324
388
|
|
|
389
|
+
// ---- search: header button opens a keyword box; Enter cycles through
|
|
390
|
+
// matching headings and jumps to each ----
|
|
391
|
+
var _s13 = react.useState(false);
|
|
392
|
+
var searchOpen = _s13[0];
|
|
393
|
+
var setSearchOpen = _s13[1];
|
|
394
|
+
var _s14 = react.useState("");
|
|
395
|
+
var query = _s14[0];
|
|
396
|
+
var setQuery = _s14[1];
|
|
397
|
+
var _s15 = react.useState(0);
|
|
398
|
+
var matchIdx = _s15[0];
|
|
399
|
+
var setMatchIdx = _s15[1];
|
|
400
|
+
var searchRef = react.useRef(null);
|
|
401
|
+
// animation state: hidden | enter | shown | out; and a transient
|
|
402
|
+
// "searching" flag that drives the spinner
|
|
403
|
+
var _s16 = react.useState("hidden");
|
|
404
|
+
var searchAnim = _s16[0];
|
|
405
|
+
var setSearchAnim = _s16[1];
|
|
406
|
+
var _s17 = react.useState(false);
|
|
407
|
+
var searching = _s17[0];
|
|
408
|
+
var setSearching = _s17[1];
|
|
409
|
+
var searchTimerRef = react.useRef(null);
|
|
410
|
+
// search scope: "title" (default) or "full" (user messages + AI replies)
|
|
411
|
+
var _s18 = react.useState("title");
|
|
412
|
+
var searchScope = _s18[0];
|
|
413
|
+
var setSearchScope = _s18[1];
|
|
414
|
+
// previous scope kept briefly so the old label cross-fades with the new one
|
|
415
|
+
var _s19 = react.useState(null);
|
|
416
|
+
var prevScope = _s19[0];
|
|
417
|
+
var setPrevScope = _s19[1];
|
|
418
|
+
var scopeTimerRef = react.useRef(null);
|
|
419
|
+
var toggleScope = function () {
|
|
420
|
+
setPrevScope(searchScope);
|
|
421
|
+
setSearchScope(searchScope === "title" ? "full" : "title");
|
|
422
|
+
setMatchIdx(0);
|
|
423
|
+
if (scopeTimerRef.current) clearTimeout(scopeTimerRef.current);
|
|
424
|
+
scopeTimerRef.current = setTimeout(function () { setPrevScope(null); }, 300);
|
|
425
|
+
};
|
|
426
|
+
var openSearch = function () {
|
|
427
|
+
setSearchOpen(true);
|
|
428
|
+
setSearchAnim("enter");
|
|
429
|
+
};
|
|
430
|
+
var closeSearch = function () {
|
|
431
|
+
setSearchAnim("out");
|
|
432
|
+
setQuery("");
|
|
433
|
+
clearHighlights();
|
|
434
|
+
setTimeout(function () {
|
|
435
|
+
setSearchOpen(false);
|
|
436
|
+
setSearchAnim("hidden");
|
|
437
|
+
}, 240);
|
|
438
|
+
};
|
|
439
|
+
react.useEffect(function () {
|
|
440
|
+
if (searchAnim === "enter") {
|
|
441
|
+
var raf = requestAnimationFrame(function () { setSearchAnim("shown"); });
|
|
442
|
+
return function () { cancelAnimationFrame(raf); };
|
|
443
|
+
}
|
|
444
|
+
return undefined;
|
|
445
|
+
}, [searchAnim]);
|
|
446
|
+
react.useEffect(function () {
|
|
447
|
+
if (searchOpen && searchRef.current) searchRef.current.focus();
|
|
448
|
+
}, [searchOpen]);
|
|
449
|
+
var onQueryChange = function (v) {
|
|
450
|
+
setQuery(v);
|
|
451
|
+
setMatchIdx(0);
|
|
452
|
+
setSearching(true);
|
|
453
|
+
clearHighlights();
|
|
454
|
+
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
455
|
+
searchTimerRef.current = setTimeout(function () { setSearching(false); }, 220);
|
|
456
|
+
};
|
|
457
|
+
|
|
325
458
|
// the group currently being read (under the list viewport middle):
|
|
326
459
|
// it stays at full opacity, every other group is dimmed
|
|
327
460
|
var _s10 = react.useState([]);
|
|
@@ -494,9 +627,10 @@ window.__ModuleLoader__.load({
|
|
|
494
627
|
var turnTimes = {};
|
|
495
628
|
var turnUserKey = {};
|
|
496
629
|
var turnUserText = {};
|
|
630
|
+
var turnUserFull = {};
|
|
497
631
|
if (!order || !nodes) return result;
|
|
498
632
|
// first pass: per-turn time — the LAST message of the turn wins (end time);
|
|
499
|
-
// also remember each turn's user message key + first-line preview
|
|
633
|
+
// also remember each turn's user message key + first-line preview + full text
|
|
500
634
|
for (var i = 0; i < order.length; i++) {
|
|
501
635
|
var k0 = order[i];
|
|
502
636
|
var n0 = nodes.get(k0);
|
|
@@ -507,11 +641,12 @@ window.__ModuleLoader__.load({
|
|
|
507
641
|
if (n0.kind === "user" && turnUserKey[tid0] === undefined) {
|
|
508
642
|
turnUserKey[tid0] = k0;
|
|
509
643
|
turnUserText[tid0] = previewText(extractUserText(n0), 30);
|
|
644
|
+
turnUserFull[tid0] = extractUserText(n0);
|
|
510
645
|
}
|
|
511
646
|
var t0 = getNodeTime(n0);
|
|
512
647
|
if (t0) turnTimes[tid0] = t0;
|
|
513
648
|
}
|
|
514
|
-
// second pass: group assistant headings by turn
|
|
649
|
+
// second pass: group assistant headings + full reply texts by turn
|
|
515
650
|
for (var j = 0; j < order.length; j++) {
|
|
516
651
|
var key = order[j];
|
|
517
652
|
var node = nodes.get(key);
|
|
@@ -525,11 +660,15 @@ window.__ModuleLoader__.load({
|
|
|
525
660
|
time: turnId !== null ? (turnTimes[turnId] || "") : "",
|
|
526
661
|
userKey: turnId !== null ? (turnUserKey[turnId] || "") : "",
|
|
527
662
|
userText: turnId !== null ? (turnUserText[turnId] || "") : "",
|
|
663
|
+
userFull: turnId !== null ? (turnUserFull[turnId] || "") : "",
|
|
664
|
+
msgs: [],
|
|
528
665
|
headings: []
|
|
529
666
|
};
|
|
530
667
|
result.push(current);
|
|
531
668
|
}
|
|
532
|
-
var
|
|
669
|
+
var text = extractReplyText(node);
|
|
670
|
+
current.msgs.push({ key: key, text: text });
|
|
671
|
+
var parsed = parseHeadings(text);
|
|
533
672
|
var hIdx = 0;
|
|
534
673
|
for (var k = 0; k < parsed.length; k++) {
|
|
535
674
|
current.headings.push({ level: parsed[k].level, title: parsed[k].title, key: key, idx: hIdx });
|
|
@@ -610,6 +749,44 @@ window.__ModuleLoader__.load({
|
|
|
610
749
|
return function () { sp.removeEventListener("scroll", update); };
|
|
611
750
|
}, [keyToGroup]);
|
|
612
751
|
|
|
752
|
+
// ---- search matches ----
|
|
753
|
+
// "title" scope: heading titles only; "full" scope: also the user
|
|
754
|
+
// message and every AI reply text of each turn.
|
|
755
|
+
// Every occurrence counts (multiple hits inside one message = multiple
|
|
756
|
+
// matches), so the n/N counter reflects the real total.
|
|
757
|
+
var matches = react.useMemo(function () {
|
|
758
|
+
var q = query.trim().toLowerCase();
|
|
759
|
+
if (!q) return [];
|
|
760
|
+
var out = [];
|
|
761
|
+
for (var gi = 0; gi < groups.length; gi++) {
|
|
762
|
+
var g = groups[gi];
|
|
763
|
+
for (var j = 0; j < g.headings.length; j++) {
|
|
764
|
+
var h = g.headings[j];
|
|
765
|
+
var n = countOccurrences(h.title, q);
|
|
766
|
+
for (var c = 0; c < n; c++) {
|
|
767
|
+
out.push({ gi: gi, title: h.title, key: h.key, idx: h.idx });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (searchScope === "full") {
|
|
771
|
+
if (g.userKey && g.userFull) {
|
|
772
|
+
var nu = countOccurrences(g.userFull, q);
|
|
773
|
+
for (var cu = 0; cu < nu; cu++) {
|
|
774
|
+
out.push({ gi: gi, title: previewText(g.userFull, 30), key: g.userKey, idx: undefined });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
for (var m = 0; m < g.msgs.length; m++) {
|
|
778
|
+
var msg = g.msgs[m];
|
|
779
|
+
if (!msg.text) continue;
|
|
780
|
+
var nm = countOccurrences(msg.text, q);
|
|
781
|
+
for (var cm = 0; cm < nm; cm++) {
|
|
782
|
+
out.push({ gi: gi, title: previewText(msg.text, 30), key: msg.key, idx: undefined });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return out;
|
|
788
|
+
}, [groups, query, searchScope]);
|
|
789
|
+
|
|
613
790
|
// ---- nothing to show without headings ----
|
|
614
791
|
if (groups.length === 0) return null;
|
|
615
792
|
|
|
@@ -686,6 +863,49 @@ window.__ModuleLoader__.load({
|
|
|
686
863
|
sp.scrollTo({ top: t, behavior: "smooth" });
|
|
687
864
|
};
|
|
688
865
|
|
|
866
|
+
// jump to the n-th match, cycling; also reveal the group in the outline.
|
|
867
|
+
// ONE smooth scroll straight to the current occurrence (no competing
|
|
868
|
+
// scrolls): highlight first, then position the mark at the upper-middle
|
|
869
|
+
var goToMatch = function (n) {
|
|
870
|
+
if (matches.length === 0) return;
|
|
871
|
+
var i = ((n % matches.length) + matches.length) % matches.length;
|
|
872
|
+
setMatchIdx(i);
|
|
873
|
+
var m = matches[i];
|
|
874
|
+
var q = query.trim().toLowerCase();
|
|
875
|
+
clearHighlights();
|
|
876
|
+
var r = findRowStrict(m.key);
|
|
877
|
+
if (r && q) {
|
|
878
|
+
// occurrence index within the target message (consecutive in matches)
|
|
879
|
+
var occ = 0;
|
|
880
|
+
for (var p = i - 1; p >= 0 && matches[p].key === m.key; p--) occ++;
|
|
881
|
+
highlightRow(r, q, occ);
|
|
882
|
+
var markEl = r.querySelector(".dqt-current");
|
|
883
|
+
var sp = r.closest ? r.closest("[data-conversation-scroll]") : null;
|
|
884
|
+
if (sp && markEl) {
|
|
885
|
+
var target = markEl.getBoundingClientRect().top - sp.getBoundingClientRect().top + sp.scrollTop - sp.clientHeight * 0.35;
|
|
886
|
+
// stick-to-bottom nudge (same as jump): break the 25px floor hold
|
|
887
|
+
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
888
|
+
if (floor - sp.scrollTop <= 25 && Math.abs(target - sp.scrollTop) > 60) {
|
|
889
|
+
sp.scrollTop = Math.max(0, floor - 26);
|
|
890
|
+
}
|
|
891
|
+
sp.scrollTo({ top: Math.max(0, target), behavior: "smooth" });
|
|
892
|
+
} else {
|
|
893
|
+
jump(m.key, m.idx);
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
jump(m.key, m.idx);
|
|
897
|
+
}
|
|
898
|
+
setVisibleCount(function (prev) {
|
|
899
|
+
return Math.max(prev, Math.min(groups.length, groups.length - m.gi));
|
|
900
|
+
});
|
|
901
|
+
setTimeout(function () {
|
|
902
|
+
var el = listRef.current;
|
|
903
|
+
if (!el) return;
|
|
904
|
+
var node = el.querySelector('[data-group-idx="' + m.gi + '"]');
|
|
905
|
+
if (node) node.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
906
|
+
}, 150);
|
|
907
|
+
};
|
|
908
|
+
|
|
689
909
|
// ---- circular icon button (glyph optical offset: ox/oy px) ----
|
|
690
910
|
var iconBtn = function (onClick, tip, glyph, fontSize, offset) {
|
|
691
911
|
var ox = offset ? (offset.x || 0) : 0;
|
|
@@ -844,15 +1064,244 @@ window.__ModuleLoader__.load({
|
|
|
844
1064
|
react_jsx_runtime.jsx("div", {
|
|
845
1065
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
846
1066
|
children: [
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
|
|
1067
|
+
// magnifier button (SVG, matches the other buttons' style)
|
|
1068
|
+
react_jsx_runtime.jsx("button", {
|
|
1069
|
+
onClick: function () { searchOpen ? closeSearch() : openSearch(); },
|
|
1070
|
+
title: "搜索标题",
|
|
1071
|
+
style: {
|
|
1072
|
+
width: 24,
|
|
1073
|
+
height: 24,
|
|
1074
|
+
display: "flex",
|
|
1075
|
+
alignItems: "center",
|
|
1076
|
+
justifyContent: "center",
|
|
1077
|
+
borderRadius: "50%",
|
|
1078
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
1079
|
+
border: "none",
|
|
1080
|
+
color: C.muted,
|
|
1081
|
+
cursor: "pointer"
|
|
1082
|
+
},
|
|
1083
|
+
onMouseEnter: function (e) {
|
|
1084
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
1085
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
1086
|
+
},
|
|
1087
|
+
onMouseLeave: function (e) {
|
|
1088
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
1089
|
+
e.currentTarget.style.color = C.muted;
|
|
1090
|
+
},
|
|
1091
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
1092
|
+
width: 15,
|
|
1093
|
+
height: 15,
|
|
1094
|
+
viewBox: "0 0 24 24",
|
|
1095
|
+
// overflow visible: the stroke may extend past the
|
|
1096
|
+
// viewBox without being clipped at the svg boundary
|
|
1097
|
+
style: { display: "block", transform: "translate(-1px, -1px)", overflow: "visible" },
|
|
1098
|
+
fill: "none",
|
|
1099
|
+
stroke: "currentColor",
|
|
1100
|
+
strokeWidth: 3.6,
|
|
1101
|
+
strokeLinecap: "round",
|
|
1102
|
+
children: [
|
|
1103
|
+
react_jsx_runtime.jsx("circle", { cx: 11, cy: 11, r: 7 }),
|
|
1104
|
+
react_jsx_runtime.jsx("line", { x1: 21, y1: 21, x2: 16, y2: 16 })
|
|
1105
|
+
]
|
|
1106
|
+
})
|
|
1107
|
+
}),
|
|
1108
|
+
// triangle tips toward the side it will move TO: shift slightly up + toward the tip
|
|
1109
|
+
iconBtn(toggleDock, dockRight ? "移到左侧" : "移到右侧", dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 }),
|
|
1110
|
+
// close: thick SVG cross, nudged slightly down
|
|
1111
|
+
react_jsx_runtime.jsx("button", {
|
|
1112
|
+
onClick: function () { setOpen(false); },
|
|
1113
|
+
title: "收起",
|
|
1114
|
+
style: {
|
|
1115
|
+
width: 24,
|
|
1116
|
+
height: 24,
|
|
1117
|
+
display: "flex",
|
|
1118
|
+
alignItems: "center",
|
|
1119
|
+
justifyContent: "center",
|
|
1120
|
+
borderRadius: "50%",
|
|
1121
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
1122
|
+
border: "none",
|
|
1123
|
+
color: C.muted,
|
|
1124
|
+
cursor: "pointer"
|
|
1125
|
+
},
|
|
1126
|
+
onMouseEnter: function (e) {
|
|
1127
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
1128
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
1129
|
+
},
|
|
1130
|
+
onMouseLeave: function (e) {
|
|
1131
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
1132
|
+
e.currentTarget.style.color = C.muted;
|
|
1133
|
+
},
|
|
1134
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
1135
|
+
width: 15,
|
|
1136
|
+
height: 15,
|
|
1137
|
+
viewBox: "0 0 24 24",
|
|
1138
|
+
style: { display: "block", transform: "translate(1px, 0px)", overflow: "visible" },
|
|
1139
|
+
fill: "none",
|
|
1140
|
+
stroke: "currentColor",
|
|
1141
|
+
strokeWidth: 3.4,
|
|
1142
|
+
strokeLinecap: "round",
|
|
1143
|
+
children: [
|
|
1144
|
+
// cross lines span 5..19, intersection (12,12) centered
|
|
1145
|
+
react_jsx_runtime.jsx("line", { x1: 5, y1: 5, x2: 19, y2: 19 }),
|
|
1146
|
+
react_jsx_runtime.jsx("line", { x1: 19, y1: 5, x2: 5, y2: 19 })
|
|
1147
|
+
]
|
|
1148
|
+
})
|
|
1149
|
+
})
|
|
850
1150
|
]
|
|
851
1151
|
})
|
|
852
1152
|
]
|
|
853
1153
|
})
|
|
854
1154
|
]
|
|
855
1155
|
}),
|
|
1156
|
+
// search input row: outer grid row animates 0fr<->1fr so the row
|
|
1157
|
+
// collapses/expands to its EXACT natural height (no max-height
|
|
1158
|
+
// overshoot stutter); the outline below moves smoothly with it
|
|
1159
|
+
(searchOpen || searchAnim === "out") ? react_jsx_runtime.jsx("div", {
|
|
1160
|
+
style: {
|
|
1161
|
+
display: "grid",
|
|
1162
|
+
gridTemplateRows: (searchAnim === "enter" || searchAnim === "out") ? "0fr" : "1fr",
|
|
1163
|
+
transition: "grid-template-rows 0.2s " + EASE,
|
|
1164
|
+
flex: "none"
|
|
1165
|
+
},
|
|
1166
|
+
children: react_jsx_runtime.jsx("div", {
|
|
1167
|
+
style: { overflow: "hidden", minHeight: 0 },
|
|
1168
|
+
children: react_jsx_runtime.jsx("div", {
|
|
1169
|
+
style: {
|
|
1170
|
+
display: "flex",
|
|
1171
|
+
alignItems: "center",
|
|
1172
|
+
gap: 4,
|
|
1173
|
+
padding: "2px 8px 4px",
|
|
1174
|
+
borderBottom: "1px solid " + C.panelBorder,
|
|
1175
|
+
opacity: (searchAnim === "enter" || searchAnim === "out") ? 0 : 1,
|
|
1176
|
+
transform: (searchAnim === "enter" || searchAnim === "out") ? "translateY(-6px)" : "none",
|
|
1177
|
+
transition: "opacity 0.2s ease, transform 0.2s ease"
|
|
1178
|
+
},
|
|
1179
|
+
children: [
|
|
1180
|
+
// input with a custom animated placeholder: the box stays static,
|
|
1181
|
+
// only the placeholder text fades when the scope switches
|
|
1182
|
+
react_jsx_runtime.jsx("div", {
|
|
1183
|
+
style: {
|
|
1184
|
+
position: "relative",
|
|
1185
|
+
flex: 1,
|
|
1186
|
+
minWidth: 0,
|
|
1187
|
+
display: "flex",
|
|
1188
|
+
alignItems: "center",
|
|
1189
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1190
|
+
borderRadius: 999,
|
|
1191
|
+
padding: "6px 12px"
|
|
1192
|
+
},
|
|
1193
|
+
children: [
|
|
1194
|
+
react_jsx_runtime.jsx("input", {
|
|
1195
|
+
ref: searchRef,
|
|
1196
|
+
value: query,
|
|
1197
|
+
onChange: function (e) { onQueryChange(e.target.value); },
|
|
1198
|
+
onKeyDown: function (e) {
|
|
1199
|
+
if (e.key === "Enter") { goToMatch(matchIdx + 1); e.preventDefault(); }
|
|
1200
|
+
if (e.key === "Escape") { closeSearch(); }
|
|
1201
|
+
},
|
|
1202
|
+
style: {
|
|
1203
|
+
flex: 1,
|
|
1204
|
+
minWidth: 0,
|
|
1205
|
+
width: "100%",
|
|
1206
|
+
background: "transparent",
|
|
1207
|
+
border: "none",
|
|
1208
|
+
outline: "none",
|
|
1209
|
+
fontSize: 13,
|
|
1210
|
+
color: C.text,
|
|
1211
|
+
padding: 0
|
|
1212
|
+
}
|
|
1213
|
+
}),
|
|
1214
|
+
(!query) ? react_jsx_runtime.jsx("span", {
|
|
1215
|
+
style: {
|
|
1216
|
+
position: "absolute",
|
|
1217
|
+
left: 12,
|
|
1218
|
+
pointerEvents: "none",
|
|
1219
|
+
fontSize: 13,
|
|
1220
|
+
color: "var(--dsw-alias-label-tertiary, rgba(128,128,128,0.7))"
|
|
1221
|
+
},
|
|
1222
|
+
children: [
|
|
1223
|
+
"搜索",
|
|
1224
|
+
react_jsx_runtime.jsx("span", {
|
|
1225
|
+
style: { position: "relative", display: "inline-block" },
|
|
1226
|
+
children: [
|
|
1227
|
+
react_jsx_runtime.jsx("span", {
|
|
1228
|
+
key: searchScope,
|
|
1229
|
+
style: { display: "inline-block", animation: "dqt-fade-in 0.25s linear" },
|
|
1230
|
+
children: searchScope === "title" ? "标题" : "全文"
|
|
1231
|
+
}),
|
|
1232
|
+
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
1233
|
+
key: "old-" + prevScope,
|
|
1234
|
+
style: { position: "absolute", left: 0, top: 0, opacity: 0, animation: "dqt-fade-out 0.25s linear forwards" },
|
|
1235
|
+
children: prevScope === "title" ? "标题" : "全文"
|
|
1236
|
+
}) : null
|
|
1237
|
+
]
|
|
1238
|
+
}),
|
|
1239
|
+
",回车定位…"
|
|
1240
|
+
]
|
|
1241
|
+
}) : null,
|
|
1242
|
+
]
|
|
1243
|
+
}),
|
|
1244
|
+
searching ? react_jsx_runtime.jsx("span", {
|
|
1245
|
+
style: { width: 13, height: 13, flex: "none", display: "flex", alignItems: "center", justifyContent: "center" },
|
|
1246
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
1247
|
+
width: 12,
|
|
1248
|
+
height: 12,
|
|
1249
|
+
viewBox: "0 0 24 24",
|
|
1250
|
+
style: { display: "block", animation: "dqt-spin 0.8s linear infinite" },
|
|
1251
|
+
children: react_jsx_runtime.jsx("circle", {
|
|
1252
|
+
cx: 12, cy: 12, r: 9,
|
|
1253
|
+
fill: "none",
|
|
1254
|
+
stroke: C.muted,
|
|
1255
|
+
strokeWidth: 3,
|
|
1256
|
+
strokeDasharray: "42 22",
|
|
1257
|
+
strokeLinecap: "round"
|
|
1258
|
+
})
|
|
1259
|
+
})
|
|
1260
|
+
}) : react_jsx_runtime.jsx("span", {
|
|
1261
|
+
style: { fontSize: 11, color: C.muted, flex: "none", minWidth: 30, textAlign: "center" },
|
|
1262
|
+
children: matches.length > 0 ? ((matchIdx % matches.length) + 1) + "/" + matches.length : ""
|
|
1263
|
+
}),
|
|
1264
|
+
// scope toggle: 标题 <-> 全文 (round pill; background fades,
|
|
1265
|
+
// label text cross-fades old->new)
|
|
1266
|
+
react_jsx_runtime.jsx("button", {
|
|
1267
|
+
onClick: toggleScope,
|
|
1268
|
+
title: searchScope === "title" ? "当前:仅搜索标题。点击切换为全文搜索" : "当前:全文搜索。点击切换为仅标题",
|
|
1269
|
+
style: {
|
|
1270
|
+
flex: "none",
|
|
1271
|
+
width: 34,
|
|
1272
|
+
height: 34,
|
|
1273
|
+
display: "flex",
|
|
1274
|
+
alignItems: "center",
|
|
1275
|
+
justifyContent: "center",
|
|
1276
|
+
borderRadius: "50%",
|
|
1277
|
+
fontSize: 11,
|
|
1278
|
+
color: searchScope === "full" ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
1279
|
+
background: searchScope === "full" ? "rgba(79,140,255,0.18)" : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1280
|
+
border: "none",
|
|
1281
|
+
cursor: "pointer",
|
|
1282
|
+
whiteSpace: "nowrap",
|
|
1283
|
+
transition: "background 0.25s ease"
|
|
1284
|
+
},
|
|
1285
|
+
children: react_jsx_runtime.jsx("span", {
|
|
1286
|
+
style: { position: "relative", display: "inline-flex", alignItems: "center", justifyContent: "center" },
|
|
1287
|
+
children: [
|
|
1288
|
+
react_jsx_runtime.jsx("span", {
|
|
1289
|
+
key: searchScope,
|
|
1290
|
+
style: { animation: "dqt-fade-in 0.25s linear", display: "block" },
|
|
1291
|
+
children: searchScope === "title" ? "标题" : "全文"
|
|
1292
|
+
}),
|
|
1293
|
+
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
1294
|
+
key: "old-" + prevScope,
|
|
1295
|
+
style: { position: "absolute", opacity: 0, animation: "dqt-fade-out 0.25s linear forwards", display: "block" },
|
|
1296
|
+
children: prevScope === "title" ? "标题" : "全文"
|
|
1297
|
+
}) : null
|
|
1298
|
+
]
|
|
1299
|
+
})
|
|
1300
|
+
})
|
|
1301
|
+
]
|
|
1302
|
+
})
|
|
1303
|
+
})
|
|
1304
|
+
}) : null,
|
|
856
1305
|
// outline list (paged: newest first, scroll to the top loads older);
|
|
857
1306
|
// the scrollbar follows the dock side (rtl flips it to the left)
|
|
858
1307
|
react_jsx_runtime.jsx("div", {
|
|
@@ -1131,6 +1580,17 @@ window.__ModuleLoader__.load({
|
|
|
1131
1580
|
|
|
1132
1581
|
exports.apply = apply;
|
|
1133
1582
|
exports.inject = inject;
|
|
1583
|
+
|
|
1584
|
+
// inject the small keyframe stylesheet once (spinner rotation)
|
|
1585
|
+
(function () {
|
|
1586
|
+
if (typeof document === "undefined") return;
|
|
1587
|
+
if (document.getElementById("dsh-quick-toc-css")) return;
|
|
1588
|
+
var s = document.createElement("style");
|
|
1589
|
+
s.id = "dsh-quick-toc-css";
|
|
1590
|
+
s.textContent = "@keyframes dqt-spin{to{transform:rotate(360deg)}}@keyframes dqt-fade-in{from{opacity:0}to{opacity:1}}@keyframes dqt-fade-out{from{opacity:1}to{opacity:0}}";
|
|
1591
|
+
document.head.appendChild(s);
|
|
1592
|
+
})();
|
|
1593
|
+
|
|
1134
1594
|
return module.exports;
|
|
1135
1595
|
}
|
|
1136
1596
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quick-toc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Quick conversation TOC for DeepSeek Harness: markdown heading outline grouped by turn, with auto-follow highlight and smooth jump navigation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|