dsh-quick-toc 0.7.14 → 0.8.0

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to **dsh-quick-toc** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.8.0] - 2026-09-29
6
+
7
+ ### Changed
8
+ - **The hover preview can be pinned**: a pin button on the card keeps it on screen after the pointer moves away, and its text can be selected; click the pin again, press Escape or click elsewhere to drop it. One pinned card at a time, and it stays where it was put.
9
+ - **Two previews side by side**: while a card is pinned, hovering another row opens a second card next to it — on whichever side has more room — so two places can be compared.
10
+ - **A context menu on outline rows**: right-click a heading row for "Pin this preview / Unpin" and "Jump to the end of this section".
11
+ - **Cards and menus animate in and out**: the pinned card, the hover preview and the row menu all fade in and out instead of appearing or vanishing instantly.
12
+
5
13
  ## [0.7.14] - 2026-09-28
6
14
 
7
15
  ### Changed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  **dsh-quick-toc** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.8.0] - 2026-09-29
6
+
7
+ ### 变更
8
+ - **悬停预览可以钉住**:预览卡右上角多了一个图钉,点一下把它钉在屏幕上——鼠标移开也不消失,文字可以选中;再点一次图钉、按 Esc 或点别处取消。同时只保留一张钉住的卡,钉住的位置不会再动。
9
+ - **钉住后可以两处对照**:钉住状态下悬停别的行会同时显示第二张卡,两张不重叠——第二张放在空得多的一侧。
10
+ - **大纲行右键菜单**:在标题行上点右键可以「钉住预览/取消钉住」或「跳到本节末尾」。
11
+ - **卡片与菜单的收放都有动画**:钉住的卡、悬停预览和右键菜单都以淡入淡出出现和消失,不再瞬间弹出或消失。
12
+
5
13
  ## [0.7.14] - 2026-09-28
6
14
 
7
15
  ### 变更
package/README.en.md CHANGED
@@ -42,7 +42,8 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
42
42
 
43
43
  | Plugin | Supported DSH |
44
44
  | --- | --- |
45
- | **0.7.x** (latest, 0.7.13) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1 |
45
+ | **0.8.x** (latest, 0.8.0) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1 |
46
+ | 0.7.x (0.7.14) | 0.1.5-rc.3, 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1 |
46
47
  | 0.6.x (0.6.3) | 0.1.5-rc.1, 0.1.5-rc.2 |
47
48
  | 0.5.x (0.5.1) | 0.1.5-rc.1, 0.1.5-rc.2 |
48
49
  | 0.4.x (0.4.1) | 0.1.5-rc.1, 0.1.5-rc.2 |
package/README.md CHANGED
@@ -42,7 +42,8 @@
42
42
 
43
43
  | 插件版本 | 支持的 DSH 版本 |
44
44
  | --- | --- |
45
- | **0.7.x**(最新,0.7.13) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1 |
45
+ | **0.8.x**(最新,0.8.0) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1 |
46
+ | 0.7.x(0.7.14) | 0.1.5-rc.3、0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1 |
46
47
  | 0.6.x(0.6.3) | 0.1.5-rc.1、0.1.5-rc.2 |
47
48
  | 0.5.x(0.5.1) | 0.1.5-rc.1、0.1.5-rc.2 |
48
49
  | 0.4.x(0.4.1) | 0.1.5-rc.1、0.1.5-rc.2 |
package/lib/client.js CHANGED
@@ -148,6 +148,11 @@ window.__ModuleLoader__.load({
148
148
  "time.yesterday": "昨天",
149
149
  "time.beforeYesterday": "前天",
150
150
  "preview.truncated": "预览由宿主提供,可能被截断",
151
+ "preview.pin": "钉住预览",
152
+ "preview.unpin": "取消钉住",
153
+ "menu.pin": "钉住预览",
154
+ "menu.unpin": "取消钉住",
155
+ "menu.end": "跳到本节末尾",
151
156
  "resize.w": "拖拽调整宽度",
152
157
  "resize.h": "拖拽调整高度",
153
158
  "resize.wh": "拖拽同时调整宽高",
@@ -246,6 +251,11 @@ window.__ModuleLoader__.load({
246
251
  // carries the numeric date. Yesterday itself reads as the word in both.
247
252
  "time.yesterday": "yesterday",
248
253
  "preview.truncated": "Preview comes from the host and may be truncated",
254
+ "preview.pin": "Pin this preview",
255
+ "preview.unpin": "Unpin",
256
+ "menu.pin": "Pin this preview",
257
+ "menu.unpin": "Unpin this preview",
258
+ "menu.end": "Jump to the end of this section",
249
259
  "resize.w": "Drag to resize the width",
250
260
  "resize.h": "Drag to resize the height",
251
261
  "resize.wh": "Drag to resize width and height",
@@ -1981,7 +1991,10 @@ window.__ModuleLoader__.load({
1981
1991
  ghost: info.ghost,
1982
1992
  top: r.top,
1983
1993
  left: r.left,
1984
- right: r.right
1994
+ right: r.right,
1995
+ // the row this card describes: pinning keeps it, and the menu labels itself from it
1996
+ key: info.key,
1997
+ idx: info.idx
1985
1998
  });
1986
1999
  }, 260);
1987
2000
  };
@@ -1997,6 +2010,152 @@ window.__ModuleLoader__.load({
1997
2010
  }, 240);
1998
2011
  };
1999
2012
 
2013
+ // ---- pinned preview + the row menu (0.8.0) --------------------------------
2014
+ // Pinning freezes one card where it is, so the pointer can move on and a second
2015
+ // row can be hovered next to it: two previews read side by side. One pin at a
2016
+ // time — pinning another row replaces it (the newest pin is the one that stays).
2017
+ var _sPin = react.useState(null);
2018
+ var pinnedCard = _sPin[0];
2019
+ var setPinnedCard = _sPin[1];
2020
+ var pinFromCard = function (card, opts) {
2021
+ if (!card) return;
2022
+ if (pinCloseTimerRef.current) { clearTimeout(pinCloseTimerRef.current); pinCloseTimerRef.current = null; }
2023
+ setPinnedClosing(false);
2024
+ setPinnedCard({
2025
+ title: card.title,
2026
+ sub: card.sub,
2027
+ preview: card.preview,
2028
+ meta: card.meta,
2029
+ ghost: card.ghost,
2030
+ key: card.key,
2031
+ top: card.top,
2032
+ left: card.left,
2033
+ right: card.right,
2034
+ // adopting the card that is already on screen must not re-play its entrance
2035
+ // animation, or pinning would blink the very card the reader is looking at
2036
+ fresh: !!(opts && opts.fresh)
2037
+ });
2038
+ // the card the reader is looking at BECOMES the pinned one: the hover card is
2039
+ // dropped in the same frame, so pinning never moves what is on screen
2040
+ if (hoverCloseTimerRef.current) { clearTimeout(hoverCloseTimerRef.current); hoverCloseTimerRef.current = null; }
2041
+ setHoverClosing(false);
2042
+ setHoverCard(null);
2043
+ };
2044
+ // The pinned card STAYS where it was put: scrolling the outline leaves it alone.
2045
+ // Dropping it runs the same fade the hover card uses, so nothing ever pops out.
2046
+ var _sPinClose = react.useState(false);
2047
+ var pinnedClosing = _sPinClose[0];
2048
+ var setPinnedClosing = _sPinClose[1];
2049
+ var pinCloseTimerRef = react.useRef(null);
2050
+ var unpinCard = function () {
2051
+ if (!pinnedCard || pinCloseTimerRef.current) return;
2052
+ setPinnedClosing(true);
2053
+ pinCloseTimerRef.current = setTimeout(function () {
2054
+ pinCloseTimerRef.current = null;
2055
+ setPinnedCard(null);
2056
+ setPinnedClosing(false);
2057
+ }, 180);
2058
+ };
2059
+ // the pinned card measures itself so the second card can be placed against it
2060
+ var pinnedRef = react.useRef(null);
2061
+ // the card keeps the row it describes, so the menu can say pin/unpin for it
2062
+ var isPinned = function (key) { return !!pinnedCard && !!key && pinnedCard.key === key; };
2063
+
2064
+ // A right-click on an outline row opens a small menu instead of the browser's:
2065
+ // pin/unpin this row's preview and jump to the end of its section.
2066
+ var _sMenu = react.useState(null);
2067
+ var rowMenu = _sMenu[0];
2068
+ var setRowMenu = _sMenu[1];
2069
+ // the menu closes through a short fade like every other card
2070
+ var _sMenuClose = react.useState(false);
2071
+ var menuClosing = _sMenuClose[0];
2072
+ var setMenuClosing = _sMenuClose[1];
2073
+ var menuCloseTimerRef = react.useRef(null);
2074
+ var openRowMenu = function (e, info, key, idx) {
2075
+ if (e && e.preventDefault) e.preventDefault();
2076
+ if (e && e.stopPropagation) e.stopPropagation();
2077
+ var x = e && typeof e.clientX === "number" ? e.clientX : 0;
2078
+ var y = e && typeof e.clientY === "number" ? e.clientY : 0;
2079
+ if (menuCloseTimerRef.current) { clearTimeout(menuCloseTimerRef.current); menuCloseTimerRef.current = null; }
2080
+ setMenuClosing(false);
2081
+ // pinning from the menu puts the card where the row is, not under the pointer
2082
+ var rect = e && e.currentTarget && e.currentTarget.getBoundingClientRect ? e.currentTarget.getBoundingClientRect() : null;
2083
+ setRowMenu({
2084
+ x: x,
2085
+ y: y,
2086
+ info: info,
2087
+ key: key,
2088
+ idx: idx,
2089
+ rowTop: rect ? rect.top : undefined,
2090
+ rowLeft: rect ? rect.left : undefined,
2091
+ rowRight: rect ? rect.right : undefined
2092
+ });
2093
+ };
2094
+ var closeRowMenu = function () {
2095
+ if (!rowMenu || menuCloseTimerRef.current) return;
2096
+ setMenuClosing(true);
2097
+ menuCloseTimerRef.current = setTimeout(function () {
2098
+ menuCloseTimerRef.current = null;
2099
+ setRowMenu(null);
2100
+ setMenuClosing(false);
2101
+ }, 150);
2102
+ };
2103
+ // Esc / a click anywhere else closes the menu and drops the pin; a pinned card
2104
+ // also goes away when the panel closes or the surface switches (below).
2105
+ // The listeners are bound ONCE and read the live state through a ref: when they were
2106
+ // re-bound on every state change the registered closure ran one step behind (Esc
2107
+ // closed the menu a second time and never reached the pin).
2108
+ var latestPin = react.useRef({});
2109
+ latestPin.current = {
2110
+ pinned: !!pinnedCard,
2111
+ menu: !!rowMenu,
2112
+ unpin: unpinCard,
2113
+ closeMenu: closeRowMenu
2114
+ };
2115
+ react.useEffect(function () {
2116
+ var onDown = function (ev) {
2117
+ var L = latestPin.current;
2118
+ if (!L.pinned && !L.menu) return;
2119
+ // a right-click is how the row menu opens: it must not drop the pin the reader
2120
+ // is comparing against
2121
+ if (ev.button === 2) return;
2122
+ var t = ev.target;
2123
+ if (t && t.closest && (t.closest("[data-dqt-pin]") || t.closest("[data-dqt-menu]"))) return;
2124
+ L.closeMenu();
2125
+ if (L.pinned && t && t.closest && t.closest("[data-dqt-pinned]")) return;
2126
+ L.unpin();
2127
+ };
2128
+ var onKey = function (ev) {
2129
+ var L = latestPin.current;
2130
+ if (ev.key !== "Escape") return;
2131
+ // Esc takes the menu first, then the pin; only when neither is up does it
2132
+ // reach the panel's own handler and close the panel.
2133
+ if (L.menu) { L.closeMenu(); ev.stopPropagation(); ev.preventDefault(); return; }
2134
+ if (L.pinned) { L.unpin(); ev.stopPropagation(); ev.preventDefault(); }
2135
+ };
2136
+ document.addEventListener("mousedown", onDown, true);
2137
+ document.addEventListener("keydown", onKey, true);
2138
+ return function () {
2139
+ document.removeEventListener("mousedown", onDown, true);
2140
+ document.removeEventListener("keydown", onKey, true);
2141
+ };
2142
+ }, []);
2143
+ // a pinned card belongs to the surface it was pinned on: closing the panel or
2144
+ // switching between the panel and the curtain drops it (through the same fade).
2145
+ // These act on the TRANSITION only — a plain re-run must never touch a live pin.
2146
+ var wasOpenRef = react.useRef(open);
2147
+ react.useEffect(function () {
2148
+ if (wasOpenRef.current === open) return;
2149
+ wasOpenRef.current = open;
2150
+ if (!open) unpinCard();
2151
+ }, [open]);
2152
+ var wasSheetRef = react.useRef(sheetOpen);
2153
+ react.useEffect(function () {
2154
+ if (wasSheetRef.current === sheetOpen) return;
2155
+ wasSheetRef.current = sheetOpen;
2156
+ unpinCard();
2157
+ }, [sheetOpen]);
2158
+
2000
2159
  // ---- the outline belongs to the CHAT view only: when the center column
2001
2160
  // switches to another view (trajectory / context / plugin views), fade the
2002
2161
  // panel and its collapsed handle out. Polled lightly because view switches
@@ -4747,6 +4906,8 @@ window.__ModuleLoader__.load({
4747
4906
  onOpenTurn: openTurn,
4748
4907
  hoverStart: hoverStart,
4749
4908
  hoverEnd: hoverEnd,
4909
+ // right-click on a row: pin its preview / jump to the end of its section
4910
+ rowMenu: openRowMenu,
4750
4911
  // the curtain is wide: rows switch to a one-line, multi-part layout that
4751
4912
  // puts the section's opening text out to the right instead of under the
4752
4913
  // title (see renderItem / renderResultRow)
@@ -6197,42 +6358,111 @@ window.__ModuleLoader__.load({
6197
6358
  // Rendered INSIDE the body portal (position: fixed) so the outline's own
6198
6359
  // scroll container can never clip it, and pointer-events: none so it can
6199
6360
  // never steal the hover that opened it.
6200
- var hoverEl = null;
6201
- if (hoverCard && open) {
6361
+ // One card body, used for both the hover card and the pinned one: same width,
6362
+ // same fields, same layering. `opts.pinned` turns it into the frozen copy (it
6363
+ // takes pointer events, so its text can be selected and its pin can be clicked);
6364
+ // the hover card stays click-through except for its pin button.
6365
+ var renderPreviewCard = function (card, opts) {
6202
6366
  var cardW = 264;
6203
- var cardTop = Math.max(8, Math.min(hoverCard.top - 4, window.innerHeight - 210));
6204
- var cardLeft = dockRight
6205
- ? Math.max(8, hoverCard.left - cardW - 10)
6206
- : Math.min(window.innerWidth - cardW - 8, hoverCard.right + 10);
6207
- var cardBody = hoverCard.preview || hoverCard.sub || "";
6208
- var cardChildren = [
6367
+ var top = Math.max(8, Math.min(card.top - 4, window.innerHeight - 210));
6368
+ var left = dockRight
6369
+ ? Math.max(8, card.left - cardW - 10)
6370
+ : Math.min(window.innerWidth - cardW - 8, card.right + 10);
6371
+ // With a card already pinned, a second one on the same band would cover it: put
6372
+ // the hover card on whichever side of the pinned card has more room (measured,
6373
+ // so a short pinned card does not push the second one further than needed).
6374
+ if (!opts.pinned && pinnedCard && pinnedVisible && !pinnedClosing) {
6375
+ var pinRect = pinnedRef.current && pinnedRef.current.getBoundingClientRect ? pinnedRef.current.getBoundingClientRect() : null;
6376
+ var pinTop = pinRect && pinRect.height > 0 ? pinRect.top : Math.max(8, Math.min(pinnedCard.top - 4, window.innerHeight - 210));
6377
+ var pinH = pinRect && pinRect.height > 0 ? pinRect.height : 210;
6378
+ var pinLeft = pinRect && pinRect.width > 0
6379
+ ? pinRect.left
6380
+ : (dockRight ? Math.max(8, pinnedCard.left - cardW - 10) : Math.min(window.innerWidth - cardW - 8, pinnedCard.right + 10));
6381
+ var overlaps = Math.abs(pinLeft - left) < cardW && top < pinTop + pinH + 6 && pinTop < top + 210 + 6;
6382
+ if (overlaps) {
6383
+ var belowRoom = window.innerHeight - 8 - (pinTop + pinH + 8);
6384
+ var aboveRoom = pinTop - 8 - 8;
6385
+ top = belowRoom >= aboveRoom
6386
+ ? Math.max(8, Math.min(pinTop + pinH + 8, window.innerHeight - 210 - 8))
6387
+ : Math.max(8, pinTop - 210 - 8);
6388
+ }
6389
+ }
6390
+ var body = card.preview || card.sub || "";
6391
+ var kids = [
6209
6392
  react_jsx_runtime.jsx("div", {
6210
- style: { fontSize: 12, fontWeight: 600, color: C.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
6211
- children: hoverCard.title
6212
- }, "hc-t")
6393
+ style: { display: "flex", alignItems: "flex-start", gap: 6 },
6394
+ children: [
6395
+ react_jsx_runtime.jsx("div", {
6396
+ style: { flex: "1 1 auto", minWidth: 0, fontSize: 12, fontWeight: 600, color: C.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
6397
+ children: card.title
6398
+ }, "hc-t"),
6399
+ react_jsx_runtime.jsx("button", {
6400
+ "data-dqt-pin": opts.pinned ? "on" : "off",
6401
+ title: opts.pinned ? T("preview.unpin") : T("preview.pin"),
6402
+ onClick: function (e) {
6403
+ e.preventDefault();
6404
+ e.stopPropagation();
6405
+ if (opts.pinned) unpinCard();
6406
+ else pinFromCard(card);
6407
+ },
6408
+ style: {
6409
+ flex: "none",
6410
+ width: 20,
6411
+ height: 20,
6412
+ marginTop: -2,
6413
+ marginRight: -4,
6414
+ display: "flex",
6415
+ alignItems: "center",
6416
+ justifyContent: "center",
6417
+ border: "none",
6418
+ borderRadius: 6,
6419
+ background: opts.pinned ? C.hover : "transparent",
6420
+ color: opts.pinned ? C.text : C.muted,
6421
+ cursor: "pointer",
6422
+ // the hover card is click-through except for this button, so reaching
6423
+ // for it does not depend on the row staying hovered
6424
+ pointerEvents: "auto",
6425
+ padding: 0,
6426
+ fontSize: 11,
6427
+ lineHeight: "20px"
6428
+ },
6429
+ children: opts.pinned ? "\u25c9" : "\u25cb"
6430
+ }, "hc-pin")
6431
+ ]
6432
+ }, "hc-head")
6213
6433
  ];
6214
- if (hoverCard.meta) {
6215
- cardChildren.push(react_jsx_runtime.jsx("div", {
6434
+ if (card.meta) {
6435
+ kids.push(react_jsx_runtime.jsx("div", {
6216
6436
  style: { fontSize: 10, color: C.muted, opacity: 0.8, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
6217
- children: hoverCard.meta
6437
+ children: card.meta
6218
6438
  }, "hc-m"));
6219
6439
  }
6220
- cardChildren.push(react_jsx_runtime.jsx("div", {
6440
+ kids.push(react_jsx_runtime.jsx("div", {
6221
6441
  style: { fontSize: 11, lineHeight: "16px", color: C.text, opacity: 0.9, marginTop: 4, maxHeight: 116, overflow: "hidden" },
6222
- children: cardBody || (hoverCard.ghost ? T("turn.loadTip") : "")
6442
+ children: body || (card.ghost ? T("turn.loadTip") : "")
6223
6443
  }, "hc-b"));
6224
- if (hoverCard.ghost) {
6225
- cardChildren.push(react_jsx_runtime.jsx("div", {
6444
+ if (card.ghost) {
6445
+ kids.push(react_jsx_runtime.jsx("div", {
6226
6446
  style: { fontSize: 10, color: C.muted, opacity: 0.7, marginTop: 4 },
6227
6447
  children: T("preview.truncated")
6228
6448
  }, "hc-n"));
6229
6449
  }
6230
- hoverEl = react_jsx_runtime.jsx("div", {
6231
- className: "dqt-hover" + (hoverClosing ? " dqt-hover-closing" : ""),
6450
+ return react_jsx_runtime.jsx("div", {
6451
+ className: "dqt-hover" + (opts.closing ? " dqt-hover-closing" : "") + (opts.pinned ? " dqt-hover-pinned" : "")
6452
+ // a card adopted from the hover card is already on screen: skip its entrance
6453
+ + (opts.pinned && !card.fresh ? " dqt-hover-static" : ""),
6454
+ "data-dqt-pinned": opts.pinned ? "on" : undefined,
6455
+ ref: opts.pinned ? pinnedRef : undefined,
6456
+ onMouseEnter: opts.pinned ? undefined : function () {
6457
+ // the pointer reached the card: keep it open so its pin can be clicked
6458
+ if (hoverCloseTimerRef.current) { clearTimeout(hoverCloseTimerRef.current); hoverCloseTimerRef.current = null; }
6459
+ setHoverClosing(false);
6460
+ },
6461
+ onMouseLeave: opts.pinned ? undefined : function () { hoverEnd(); },
6232
6462
  style: {
6233
6463
  position: "fixed",
6234
- top: cardTop,
6235
- left: cardLeft,
6464
+ top: top,
6465
+ left: left,
6236
6466
  width: cardW,
6237
6467
  padding: "8px 10px",
6238
6468
  borderRadius: 8,
@@ -6243,11 +6473,106 @@ window.__ModuleLoader__.load({
6243
6473
  boxShadow: "0 8px 24px rgba(0,0,0,0.28)",
6244
6474
  // above the panel and the curtain — it is a sibling of both in this
6245
6475
  // portal, and both sit below it in the layer band we claim
6246
- zIndex: Z_HOVER,
6247
- pointerEvents: "none"
6476
+ zIndex: Z_HOVER + (opts.pinned ? 1 : 0),
6477
+ // a fading card is inert: it must not swallow the click that dismissed it
6478
+ // (and the pinned card is the only clickable one while it is up)
6479
+ pointerEvents: opts.closing ? "none" : (opts.pinned ? "auto" : "none"),
6480
+ userSelect: opts.pinned ? "text" : "none"
6481
+ },
6482
+ children: kids
6483
+ }, opts.pinned ? "pinned-card" : "hover-card");
6484
+ };
6485
+
6486
+ // The pinned card is FROZEN where it was put: scrolling the outline leaves it alone,
6487
+ // and it only leaves the screen through the same fade the hover card uses.
6488
+ var pinnedVisible = !!(pinnedCard && open);
6489
+ var pinnedEl = pinnedVisible
6490
+ ? renderPreviewCard(pinnedCard, { pinned: true, closing: pinnedClosing })
6491
+ : null;
6492
+ var hoverEl = null;
6493
+ if (hoverCard && open) hoverEl = renderPreviewCard(hoverCard, { pinned: false, closing: hoverClosing });
6494
+
6495
+ // ---- the row menu ---------------------------------------------------------
6496
+ // Small, fixed-position, keyboard-reachable: ↑/↓ move, Enter runs, Esc closes.
6497
+ var rowMenuEl = null;
6498
+ if (rowMenu && open) {
6499
+ var menuW = 208;
6500
+ var menuH = 76;
6501
+ var menuLeft = Math.max(8, Math.min(rowMenu.x, window.innerWidth - menuW - 8));
6502
+ var menuTop = Math.max(8, Math.min(rowMenu.y, window.innerHeight - menuH - 8));
6503
+ var menuItems = [
6504
+ {
6505
+ id: "pin",
6506
+ label: isPinned(rowMenu.key) ? T("menu.unpin") : T("menu.pin"),
6507
+ run: function () {
6508
+ if (isPinned(rowMenu.key)) unpinCard();
6509
+ else pinFromCard({
6510
+ title: rowMenu.info.title,
6511
+ sub: rowMenu.info.sub,
6512
+ preview: rowMenu.info.preview,
6513
+ meta: rowMenu.info.meta,
6514
+ ghost: rowMenu.info.ghost,
6515
+ key: rowMenu.key,
6516
+ // pin it where its ROW is, not where the right-click landed
6517
+ top: rowMenu.rowTop !== undefined ? rowMenu.rowTop : rowMenu.y,
6518
+ left: rowMenu.rowLeft !== undefined ? rowMenu.rowLeft : rowMenu.x,
6519
+ right: rowMenu.rowRight !== undefined ? rowMenu.rowRight : rowMenu.x
6520
+ }, { fresh: true });
6521
+ closeRowMenu();
6522
+ }
6248
6523
  },
6249
- children: cardChildren
6250
- }, "hover-card");
6524
+ {
6525
+ id: "end",
6526
+ label: T("menu.end"),
6527
+ run: function () {
6528
+ var key = rowMenu.key;
6529
+ var idx = rowMenu.idx;
6530
+ closeRowMenu();
6531
+ if (typeof endJumpRow === "function") endJumpRow(key, idx === undefined ? 0 : idx);
6532
+ }
6533
+ }
6534
+ ];
6535
+ rowMenuEl = react_jsx_runtime.jsx("div", {
6536
+ "data-dqt-menu": "on",
6537
+ className: "dqt-rowmenu" + (menuClosing ? " dqt-rowmenu-closing" : ""),
6538
+ role: "menu",
6539
+ style: {
6540
+ position: "fixed",
6541
+ top: menuTop,
6542
+ left: menuLeft,
6543
+ width: menuW,
6544
+ padding: 4,
6545
+ borderRadius: 8,
6546
+ background: C.panelBg,
6547
+ color: C.text,
6548
+ border: "1px solid " + C.panelBorder,
6549
+ boxShadow: "0 10px 28px rgba(0,0,0,0.32)",
6550
+ zIndex: Z_HOVER + 2,
6551
+ // a fading menu must not swallow the click that dismissed it
6552
+ pointerEvents: menuClosing ? "none" : "auto"
6553
+ },
6554
+ children: menuItems.map(function (item) {
6555
+ return react_jsx_runtime.jsx("button", {
6556
+ role: "menuitem",
6557
+ "data-dqt-menu-item": item.id,
6558
+ onClick: function (e) { e.preventDefault(); e.stopPropagation(); item.run(); },
6559
+ style: {
6560
+ display: "block",
6561
+ width: "100%",
6562
+ textAlign: "left",
6563
+ padding: "6px 8px",
6564
+ border: "none",
6565
+ borderRadius: 6,
6566
+ background: "transparent",
6567
+ color: C.text,
6568
+ cursor: "pointer",
6569
+ fontSize: 12,
6570
+ lineHeight: "18px"
6571
+ },
6572
+ children: item.label
6573
+ }, "menu-" + item.id);
6574
+ })
6575
+ }, "row-menu");
6251
6576
  }
6252
6577
 
6253
6578
  // ---- the curtain shell ---------------------------------------------------
@@ -6333,7 +6658,7 @@ window.__ModuleLoader__.load({
6333
6658
 
6334
6659
  return react_dom.createPortal(
6335
6660
  react_jsx_runtime.jsx(ErrorBoundary, {
6336
- children: [sheetShellEl, edgeEl, topHandleEl, hoverEl]
6661
+ children: [sheetShellEl, edgeEl, topHandleEl, pinnedEl, hoverEl, rowMenuEl]
6337
6662
  }),
6338
6663
  document.body
6339
6664
  );
@@ -6419,7 +6744,10 @@ window.__ModuleLoader__.load({
6419
6744
  sub: n.sub || "",
6420
6745
  preview: n.preview || "",
6421
6746
  meta: [n.time, path ? path.path : ""].filter(Boolean).join(" · "),
6422
- ghost: false
6747
+ ghost: false,
6748
+ // what pinning and the row menu need to act on this very row
6749
+ key: n.key,
6750
+ idx: n.idx
6423
6751
  };
6424
6752
  var rows;
6425
6753
  if (wide) {
@@ -6520,6 +6848,9 @@ window.__ModuleLoader__.load({
6520
6848
  if (b) b.style.opacity = "0";
6521
6849
  if (extra && extra.hoverEnd) extra.hoverEnd();
6522
6850
  },
6851
+ onContextMenu: function (e) {
6852
+ if (extra && extra.rowMenu) extra.rowMenu(e, hoverInfo, n.key, n.idx);
6853
+ },
6523
6854
  "data-jump-key": n.key,
6524
6855
  "data-jump-idx": n.idx !== undefined ? String(n.idx) : "0",
6525
6856
  // keyboard navigation walks the ORDER of these rows inside the list
@@ -7646,9 +7977,17 @@ window.__ModuleLoader__.load({
7646
7977
  // re-renders (which rewrite inline styles) cannot restart or cancel them
7647
7978
  ".dqt-levels-pop{animation:dqt-pop-in 0.22s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
7648
7979
  // hover preview card: fades in place, and fades OUT when the pointer
7649
- // leaves (both as stylesheet classes so re-renders cannot cancel them)
7980
+ // leaves (both as stylesheet classes so re-renders cannot cancel them).
7981
+ // The pinned card wears the same two classes, so pinning, unpinning, Esc, a
7982
+ // click outside and a panel close all animate; `.dqt-hover-static` is for the
7983
+ // card that is ADOPTED from the hover card (it is already on screen, so it must
7984
+ // not re-play the entrance and blink).
7650
7985
  ".dqt-hover{animation:dqt-fade-in 0.12s linear both}" +
7651
- ".dqt-hover-closing{animation:dqt-fade-out 0.16s cubic-bezier(0.2, 0.9, 0.3, 1) both}" +
7986
+ ".dqt-hover-static{animation:none}" +
7987
+ ".dqt-hover-closing{animation:dqt-fade-out 0.16s cubic-bezier(0.2, 0.9, 0.3, 1) both;pointer-events:none}" +
7988
+ // the outline row menu: same vocabulary as the level picker (a small pop)
7989
+ ".dqt-rowmenu{animation:dqt-pop-in 0.14s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
7990
+ ".dqt-rowmenu-closing{animation:dqt-pop-out 0.14s cubic-bezier(0.2, 0.9, 0.3, 1) both;pointer-events:none;will-change:transform,opacity}" +
7652
7991
  // transient edge banner over the bottom of the outline list
7653
7992
  "@keyframes dqt-toast-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}" +
7654
7993
  "@keyframes dqt-toast-out{from{opacity:1;transform:none}to{opacity:0;transform:none}}" +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quick-toc",
3
- "version": "0.7.14",
3
+ "version": "0.8.0",
4
4
  "description": "Conversation TOC for DeepSeek Harness (DSH): a whole-session, turn-grouped Markdown outline with hover previews, a level filter, a full-width curtain, jump-to-end buttons, title/full-text/cross-session search and a Chinese/English interface.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",