dsh-quick-toc 0.1.1 → 0.2.0-beta
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 +249 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -322,6 +322,57 @@ window.__ModuleLoader__.load({
|
|
|
322
322
|
var didInitScroll = react.useRef(false);
|
|
323
323
|
var outlineTouchRef = react.useRef(0); // last time the user touched the outline
|
|
324
324
|
|
|
325
|
+
// ---- search: header button opens a keyword box; Enter cycles through
|
|
326
|
+
// matching headings and jumps to each ----
|
|
327
|
+
var _s13 = react.useState(false);
|
|
328
|
+
var searchOpen = _s13[0];
|
|
329
|
+
var setSearchOpen = _s13[1];
|
|
330
|
+
var _s14 = react.useState("");
|
|
331
|
+
var query = _s14[0];
|
|
332
|
+
var setQuery = _s14[1];
|
|
333
|
+
var _s15 = react.useState(0);
|
|
334
|
+
var matchIdx = _s15[0];
|
|
335
|
+
var setMatchIdx = _s15[1];
|
|
336
|
+
var searchRef = react.useRef(null);
|
|
337
|
+
// animation state: hidden | enter | shown | out; and a transient
|
|
338
|
+
// "searching" flag that drives the spinner
|
|
339
|
+
var _s16 = react.useState("hidden");
|
|
340
|
+
var searchAnim = _s16[0];
|
|
341
|
+
var setSearchAnim = _s16[1];
|
|
342
|
+
var _s17 = react.useState(false);
|
|
343
|
+
var searching = _s17[0];
|
|
344
|
+
var setSearching = _s17[1];
|
|
345
|
+
var searchTimerRef = react.useRef(null);
|
|
346
|
+
var openSearch = function () {
|
|
347
|
+
setSearchOpen(true);
|
|
348
|
+
setSearchAnim("enter");
|
|
349
|
+
};
|
|
350
|
+
var closeSearch = function () {
|
|
351
|
+
setSearchAnim("out");
|
|
352
|
+
setQuery("");
|
|
353
|
+
setTimeout(function () {
|
|
354
|
+
setSearchOpen(false);
|
|
355
|
+
setSearchAnim("hidden");
|
|
356
|
+
}, 240);
|
|
357
|
+
};
|
|
358
|
+
react.useEffect(function () {
|
|
359
|
+
if (searchAnim === "enter") {
|
|
360
|
+
var raf = requestAnimationFrame(function () { setSearchAnim("shown"); });
|
|
361
|
+
return function () { cancelAnimationFrame(raf); };
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}, [searchAnim]);
|
|
365
|
+
react.useEffect(function () {
|
|
366
|
+
if (searchOpen && searchRef.current) searchRef.current.focus();
|
|
367
|
+
}, [searchOpen]);
|
|
368
|
+
var onQueryChange = function (v) {
|
|
369
|
+
setQuery(v);
|
|
370
|
+
setMatchIdx(0);
|
|
371
|
+
setSearching(true);
|
|
372
|
+
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
373
|
+
searchTimerRef.current = setTimeout(function () { setSearching(false); }, 220);
|
|
374
|
+
};
|
|
375
|
+
|
|
325
376
|
// the group currently being read (under the list viewport middle):
|
|
326
377
|
// it stays at full opacity, every other group is dimmed
|
|
327
378
|
var _s10 = react.useState([]);
|
|
@@ -610,6 +661,23 @@ window.__ModuleLoader__.load({
|
|
|
610
661
|
return function () { sp.removeEventListener("scroll", update); };
|
|
611
662
|
}, [keyToGroup]);
|
|
612
663
|
|
|
664
|
+
// ---- search matches: headings whose title contains the query ----
|
|
665
|
+
var matches = react.useMemo(function () {
|
|
666
|
+
var q = query.trim().toLowerCase();
|
|
667
|
+
if (!q) return [];
|
|
668
|
+
var out = [];
|
|
669
|
+
for (var gi = 0; gi < groups.length; gi++) {
|
|
670
|
+
var g = groups[gi];
|
|
671
|
+
for (var j = 0; j < g.headings.length; j++) {
|
|
672
|
+
var h = g.headings[j];
|
|
673
|
+
if (h.title.toLowerCase().indexOf(q) >= 0) {
|
|
674
|
+
out.push({ gi: gi, title: h.title, key: h.key, idx: h.idx });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return out;
|
|
679
|
+
}, [groups, query]);
|
|
680
|
+
|
|
613
681
|
// ---- nothing to show without headings ----
|
|
614
682
|
if (groups.length === 0) return null;
|
|
615
683
|
|
|
@@ -686,6 +754,24 @@ window.__ModuleLoader__.load({
|
|
|
686
754
|
sp.scrollTo({ top: t, behavior: "smooth" });
|
|
687
755
|
};
|
|
688
756
|
|
|
757
|
+
// jump to the n-th match, cycling; also reveal the group in the outline
|
|
758
|
+
var goToMatch = function (n) {
|
|
759
|
+
if (matches.length === 0) return;
|
|
760
|
+
var i = ((n % matches.length) + matches.length) % matches.length;
|
|
761
|
+
setMatchIdx(i);
|
|
762
|
+
var m = matches[i];
|
|
763
|
+
jump(m.key, m.idx);
|
|
764
|
+
setVisibleCount(function (prev) {
|
|
765
|
+
return Math.max(prev, Math.min(groups.length, groups.length - m.gi));
|
|
766
|
+
});
|
|
767
|
+
setTimeout(function () {
|
|
768
|
+
var el = listRef.current;
|
|
769
|
+
if (!el) return;
|
|
770
|
+
var node = el.querySelector('[data-group-idx="' + m.gi + '"]');
|
|
771
|
+
if (node) node.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
772
|
+
}, 150);
|
|
773
|
+
};
|
|
774
|
+
|
|
689
775
|
// ---- circular icon button (glyph optical offset: ox/oy px) ----
|
|
690
776
|
var iconBtn = function (onClick, tip, glyph, fontSize, offset) {
|
|
691
777
|
var ox = offset ? (offset.x || 0) : 0;
|
|
@@ -844,15 +930,164 @@ window.__ModuleLoader__.load({
|
|
|
844
930
|
react_jsx_runtime.jsx("div", {
|
|
845
931
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
846
932
|
children: [
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
|
|
933
|
+
// magnifier button (SVG, matches the other buttons' style)
|
|
934
|
+
react_jsx_runtime.jsx("button", {
|
|
935
|
+
onClick: function () { searchOpen ? closeSearch() : openSearch(); },
|
|
936
|
+
title: "搜索标题",
|
|
937
|
+
style: {
|
|
938
|
+
width: 24,
|
|
939
|
+
height: 24,
|
|
940
|
+
display: "flex",
|
|
941
|
+
alignItems: "center",
|
|
942
|
+
justifyContent: "center",
|
|
943
|
+
borderRadius: "50%",
|
|
944
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
945
|
+
border: "none",
|
|
946
|
+
color: C.muted,
|
|
947
|
+
cursor: "pointer"
|
|
948
|
+
},
|
|
949
|
+
onMouseEnter: function (e) {
|
|
950
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
951
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
952
|
+
},
|
|
953
|
+
onMouseLeave: function (e) {
|
|
954
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
955
|
+
e.currentTarget.style.color = C.muted;
|
|
956
|
+
},
|
|
957
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
958
|
+
width: 15,
|
|
959
|
+
height: 15,
|
|
960
|
+
viewBox: "0 0 24 24",
|
|
961
|
+
// overflow visible: the stroke may extend past the
|
|
962
|
+
// viewBox without being clipped at the svg boundary
|
|
963
|
+
style: { display: "block", transform: "translate(-1px, -1px)", overflow: "visible" },
|
|
964
|
+
fill: "none",
|
|
965
|
+
stroke: "currentColor",
|
|
966
|
+
strokeWidth: 3.6,
|
|
967
|
+
strokeLinecap: "round",
|
|
968
|
+
children: [
|
|
969
|
+
react_jsx_runtime.jsx("circle", { cx: 11, cy: 11, r: 7 }),
|
|
970
|
+
react_jsx_runtime.jsx("line", { x1: 21, y1: 21, x2: 16, y2: 16 })
|
|
971
|
+
]
|
|
972
|
+
})
|
|
973
|
+
}),
|
|
974
|
+
// triangle tips toward the side it will move TO: shift slightly up + toward the tip
|
|
975
|
+
iconBtn(toggleDock, dockRight ? "移到左侧" : "移到右侧", dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 }),
|
|
976
|
+
// close: thick SVG cross, nudged slightly down
|
|
977
|
+
react_jsx_runtime.jsx("button", {
|
|
978
|
+
onClick: function () { setOpen(false); },
|
|
979
|
+
title: "收起",
|
|
980
|
+
style: {
|
|
981
|
+
width: 24,
|
|
982
|
+
height: 24,
|
|
983
|
+
display: "flex",
|
|
984
|
+
alignItems: "center",
|
|
985
|
+
justifyContent: "center",
|
|
986
|
+
borderRadius: "50%",
|
|
987
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
988
|
+
border: "none",
|
|
989
|
+
color: C.muted,
|
|
990
|
+
cursor: "pointer"
|
|
991
|
+
},
|
|
992
|
+
onMouseEnter: function (e) {
|
|
993
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-active, rgba(79,140,255,0.24))";
|
|
994
|
+
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
995
|
+
},
|
|
996
|
+
onMouseLeave: function (e) {
|
|
997
|
+
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
998
|
+
e.currentTarget.style.color = C.muted;
|
|
999
|
+
},
|
|
1000
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
1001
|
+
width: 15,
|
|
1002
|
+
height: 15,
|
|
1003
|
+
viewBox: "0 0 24 24",
|
|
1004
|
+
style: { display: "block", transform: "translate(1px, 0px)", overflow: "visible" },
|
|
1005
|
+
fill: "none",
|
|
1006
|
+
stroke: "currentColor",
|
|
1007
|
+
strokeWidth: 3.4,
|
|
1008
|
+
strokeLinecap: "round",
|
|
1009
|
+
children: [
|
|
1010
|
+
// cross lines span 5..19, intersection (12,12) centered
|
|
1011
|
+
react_jsx_runtime.jsx("line", { x1: 5, y1: 5, x2: 19, y2: 19 }),
|
|
1012
|
+
react_jsx_runtime.jsx("line", { x1: 19, y1: 5, x2: 5, y2: 19 })
|
|
1013
|
+
]
|
|
1014
|
+
})
|
|
1015
|
+
})
|
|
850
1016
|
]
|
|
851
1017
|
})
|
|
852
1018
|
]
|
|
853
1019
|
})
|
|
854
1020
|
]
|
|
855
1021
|
}),
|
|
1022
|
+
// search input row: outer grid row animates 0fr<->1fr so the row
|
|
1023
|
+
// collapses/expands to its EXACT natural height (no max-height
|
|
1024
|
+
// overshoot stutter); the outline below moves smoothly with it
|
|
1025
|
+
(searchOpen || searchAnim === "out") ? react_jsx_runtime.jsx("div", {
|
|
1026
|
+
style: {
|
|
1027
|
+
display: "grid",
|
|
1028
|
+
gridTemplateRows: (searchAnim === "enter" || searchAnim === "out") ? "0fr" : "1fr",
|
|
1029
|
+
transition: "grid-template-rows 0.2s " + EASE,
|
|
1030
|
+
flex: "none"
|
|
1031
|
+
},
|
|
1032
|
+
children: react_jsx_runtime.jsx("div", {
|
|
1033
|
+
style: { overflow: "hidden", minHeight: 0 },
|
|
1034
|
+
children: react_jsx_runtime.jsx("div", {
|
|
1035
|
+
style: {
|
|
1036
|
+
display: "flex",
|
|
1037
|
+
alignItems: "center",
|
|
1038
|
+
gap: 4,
|
|
1039
|
+
padding: "2px 8px 4px",
|
|
1040
|
+
borderBottom: "1px solid " + C.panelBorder,
|
|
1041
|
+
opacity: (searchAnim === "enter" || searchAnim === "out") ? 0 : 1,
|
|
1042
|
+
transform: (searchAnim === "enter" || searchAnim === "out") ? "translateY(-6px)" : "none",
|
|
1043
|
+
transition: "opacity 0.2s ease, transform 0.2s ease"
|
|
1044
|
+
},
|
|
1045
|
+
children: [
|
|
1046
|
+
react_jsx_runtime.jsx("input", {
|
|
1047
|
+
ref: searchRef,
|
|
1048
|
+
value: query,
|
|
1049
|
+
onChange: function (e) { onQueryChange(e.target.value); },
|
|
1050
|
+
onKeyDown: function (e) {
|
|
1051
|
+
if (e.key === "Enter") { goToMatch(matchIdx + 1); e.preventDefault(); }
|
|
1052
|
+
if (e.key === "Escape") { closeSearch(); }
|
|
1053
|
+
},
|
|
1054
|
+
placeholder: "搜索标题,回车定位…",
|
|
1055
|
+
style: {
|
|
1056
|
+
flex: 1,
|
|
1057
|
+
minWidth: 0,
|
|
1058
|
+
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1059
|
+
border: "none",
|
|
1060
|
+
borderRadius: 6,
|
|
1061
|
+
padding: "3px 8px",
|
|
1062
|
+
fontSize: 12,
|
|
1063
|
+
color: C.text,
|
|
1064
|
+
outline: "none"
|
|
1065
|
+
}
|
|
1066
|
+
}),
|
|
1067
|
+
searching ? react_jsx_runtime.jsx("span", {
|
|
1068
|
+
style: { width: 13, height: 13, flex: "none", display: "flex", alignItems: "center", justifyContent: "center" },
|
|
1069
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
1070
|
+
width: 12,
|
|
1071
|
+
height: 12,
|
|
1072
|
+
viewBox: "0 0 24 24",
|
|
1073
|
+
style: { display: "block", animation: "dqt-spin 0.8s linear infinite" },
|
|
1074
|
+
children: react_jsx_runtime.jsx("circle", {
|
|
1075
|
+
cx: 12, cy: 12, r: 9,
|
|
1076
|
+
fill: "none",
|
|
1077
|
+
stroke: C.muted,
|
|
1078
|
+
strokeWidth: 3,
|
|
1079
|
+
strokeDasharray: "42 22",
|
|
1080
|
+
strokeLinecap: "round"
|
|
1081
|
+
})
|
|
1082
|
+
})
|
|
1083
|
+
}) : react_jsx_runtime.jsx("span", {
|
|
1084
|
+
style: { fontSize: 11, color: C.muted, flex: "none", minWidth: 30, textAlign: "center" },
|
|
1085
|
+
children: matches.length > 0 ? ((matchIdx % matches.length) + 1) + "/" + matches.length : ""
|
|
1086
|
+
})
|
|
1087
|
+
]
|
|
1088
|
+
})
|
|
1089
|
+
})
|
|
1090
|
+
}) : null,
|
|
856
1091
|
// outline list (paged: newest first, scroll to the top loads older);
|
|
857
1092
|
// the scrollbar follows the dock side (rtl flips it to the left)
|
|
858
1093
|
react_jsx_runtime.jsx("div", {
|
|
@@ -1131,6 +1366,17 @@ window.__ModuleLoader__.load({
|
|
|
1131
1366
|
|
|
1132
1367
|
exports.apply = apply;
|
|
1133
1368
|
exports.inject = inject;
|
|
1369
|
+
|
|
1370
|
+
// inject the small keyframe stylesheet once (spinner rotation)
|
|
1371
|
+
(function () {
|
|
1372
|
+
if (typeof document === "undefined") return;
|
|
1373
|
+
if (document.getElementById("dsh-quick-toc-css")) return;
|
|
1374
|
+
var s = document.createElement("style");
|
|
1375
|
+
s.id = "dsh-quick-toc-css";
|
|
1376
|
+
s.textContent = "@keyframes dqt-spin{to{transform:rotate(360deg)}}";
|
|
1377
|
+
document.head.appendChild(s);
|
|
1378
|
+
})();
|
|
1379
|
+
|
|
1134
1380
|
return module.exports;
|
|
1135
1381
|
}
|
|
1136
1382
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quick-toc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta",
|
|
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",
|