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.
Files changed (2) hide show
  1. package/lib/client.js +466 -6
  2. 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 parsed = parseHeadings(extractReplyText(node));
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
- // triangle tips toward the side it will move TO: shift slightly down + toward the tip
848
- iconBtn(toggleDock, dockRight ? "移到左侧" : "移到右侧", dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: 1 } : { x: 1, y: 1 }),
849
- iconBtn(function () { setOpen(false); }, "收起", "✕", 13, { x: 0, y: -1 })
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.1.1",
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",