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.
Files changed (2) hide show
  1. package/lib/client.js +249 -3
  2. 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
- // 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 })
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.1.1",
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",