@luziyang2026/dsh-question-nav 0.7.3 → 0.7.5

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/README.md CHANGED
@@ -21,7 +21,11 @@ Package: **`@luziyang2026/dsh-question-nav`** ([npm][npm] · [GitHub][github]).
21
21
 
22
22
  ## Preview
23
23
 
24
- ![Full DSH Web GUI screenshot with the question-nav dot rail embedded on the left edge of the conversation column](https://raw.githubusercontent.com/AbelKeithsun/dsh-question-nav/main/docs/images/question-nav-preview.jpg)
24
+ ![Full DSH Web GUI screenshot with the question-nav dot rail embedded on the left edge of the conversation column](https://raw.githubusercontent.com/AbelKeithsun/dsh-question-nav/main/docs/images/0.7.4%E6%95%88%E6%9E%9C%E9%A2%84%E8%A7%88.jpg)
25
+
26
+ Plugin settings (rail alignment + dots per page):
27
+
28
+ ![The plugin's settings page: rail alignment and dots-per-page segmented controls](https://raw.githubusercontent.com/AbelKeithsun/dsh-question-nav/main/docs/images/%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE%E5%8F%82%E6%95%B0%E8%AE%BE%E7%BD%AE.jpg)
25
29
 
26
30
  ## What it does
27
31
 
@@ -58,10 +62,13 @@ Package: **`@luziyang2026/dsh-question-nav`** ([npm][npm] · [GitHub][github]).
58
62
  whole history up front.
59
63
  - **Paged overflow**: when many dots don't fit the 60% band, the native
60
64
  scrollbar is hidden and two small triangle buttons in the dot style appear —
61
- **▲ above the dot queue and ▼ below it** — each click revealing five hidden
62
- dots. The column fades at its top and bottom edges as a pure visual cue
63
- (wheel and trackpad still scroll too); **hovering never scrolls the band**,
64
- so the dots stay put while you browse.
65
+ **▲ above the dot queue and ▼ below it** — each click revealing the next
66
+ page of hidden dots, which pop in with a short staggered animation as click
67
+ feedback. The page size is configurable **Settings Plugins Question
68
+ Nav Dots per page** (3 / 5 / 8 / 10, default 5). The triangles themselves
69
+ are the overflow cue — each appears only while its direction has more dots
70
+ to reveal (wheel and trackpad still scroll too); **hovering never scrolls
71
+ the band**, so the dots stay put while you browse.
65
72
  - Empty/left areas of the rail pass pointer events through to the conversation
66
73
  (it never blocks the chat).
67
74
 
package/README.zh.md CHANGED
@@ -18,7 +18,11 @@
18
18
 
19
19
  ## 效果展示
20
20
 
21
- ![DSH Web GUI 完整截图:对话栏左侧内嵌提问导航圆点列,每个圆点对应一个用户提问](https://raw.githubusercontent.com/AbelKeithsun/dsh-question-nav/main/docs/images/question-nav-preview.jpg)
21
+ ![DSH Web GUI 完整截图:对话栏左侧内嵌提问导航圆点列,每个圆点对应一个用户提问](https://raw.githubusercontent.com/AbelKeithsun/dsh-question-nav/main/docs/images/0.7.4%E6%95%88%E6%9E%9C%E9%A2%84%E8%A7%88.jpg)
22
+
23
+ 插件设置页(导航条对齐 + 每次翻出数量):
24
+
25
+ ![插件设置页截图:导航条对齐与每次翻出数量两组分段选项](https://raw.githubusercontent.com/AbelKeithsun/dsh-question-nav/main/docs/images/%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE%E5%8F%82%E6%95%B0%E8%AE%BE%E7%BD%AE.jpg)
22
26
 
23
27
  ## 功能
24
28
 
@@ -47,8 +51,11 @@
47
51
  带进视图——绝不会一次性展开整个历史。
48
52
  - **分页式溢出**:圆点太多超出 60% 限高时,原生滚动条被隐藏,点列上下会出现
49
53
  两个与圆点同风格的三角按钮——**队列上方 ▲、队列下方 ▼**——每次点击翻出
50
- 5 个隐藏圆点。点列上下边缘的渐隐只是纯视觉提示(滚轮/触控板同样可滚);
51
- **悬停绝不滚动点列**,浏览时点列保持不动。
54
+ 下一页隐藏圆点,新翻出的圆点会带一个简短的逐个点弹入动画作为点击反馈。
55
+ 每页数量可在 **设置 → 插件 → 提问导航 → 每次翻出数量** 中配置
56
+ (3 / 5 / 8 / 10,默认 5)。三角按钮本身就是溢出提示——只有对应方向还有
57
+ 更多圆点时它才会出现(滚轮/触控板同样可滚);**悬停绝不滚动点列**,
58
+ 浏览时点列保持不动。
52
59
  - 圆点列空白区域**点穿**到对话内容,不会挡住聊天。
53
60
 
54
61
  ## 环境要求
package/lib/client.js CHANGED
@@ -186,7 +186,7 @@ window.__ModuleLoader__.load({
186
186
  }
187
187
  //#endregion
188
188
  //#region \0dsh-css:dsh-question-nav/src/client/question-nav.module.css.mjs
189
- const css = ".TWf_pa_rail{z-index:1;box-sizing:border-box;pointer-events:none;background:0 0;flex-direction:column;align-items:center;width:44px;display:flex;position:absolute;top:0;bottom:0;left:0}.TWf_pa_railRight{left:auto}.TWf_pa_queue{box-sizing:border-box;pointer-events:none;flex-direction:column;flex:0 auto;align-items:center;gap:6px;width:100%;min-height:0;max-height:60%;margin:auto 0;display:flex}.TWf_pa_list{scrollbar-width:none;pointer-events:auto;flex-direction:column;flex:auto;align-items:center;gap:6px;width:100%;min-height:0;padding:8px 0;display:flex;overflow:hidden auto}.TWf_pa_list::-webkit-scrollbar{width:0;height:0;display:none}.TWf_pa_listScrollable{-webkit-mask-image:linear-gradient(#0000,#000 22px calc(100% - 22px),#0000);mask-image:linear-gradient(#0000,#000 22px calc(100% - 22px),#0000)}.TWf_pa_dot{pointer-events:auto;background:var(--dsw-alias-border-l3);cursor:pointer;border:none;border-radius:50%;flex:none;width:8px;height:8px;padding:0;transition:transform .12s,background .12s}.TWf_pa_dot:hover,.TWf_pa_dot.TWf_pa_focused,.TWf_pa_dot.TWf_pa_active{background:var(--dsw-alias-brand-primary)}.TWf_pa_count{color:var(--dsw-alias-label-tertiary);user-select:none;flex:none;font-size:10px;font-weight:600;line-height:1}.TWf_pa_dots{flex-direction:column;align-items:center;gap:6px;display:flex}.TWf_pa_navBtn{pointer-events:auto;cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;width:14px;height:12px;padding:0;display:flex}.TWf_pa_navBtn:before{content:\"\";border-left:5px solid #0000;border-right:5px solid #0000;width:0;height:0;transition:border-color .12s;display:block}.TWf_pa_navUp:before{border-bottom:7px solid var(--dsw-alias-border-l3)}.TWf_pa_navDown:before{border-top:7px solid var(--dsw-alias-border-l3)}.TWf_pa_navUp:hover:before{border-bottom-color:var(--dsw-alias-brand-primary)}.TWf_pa_navDown:hover:before{border-top-color:var(--dsw-alias-brand-primary)}.TWf_pa_empty{color:var(--dsw-alias-label-tertiary);text-align:center;word-break:break-word;padding:10px 4px;font-size:11px}.TWf_pa_hint{z-index:30;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);max-width:260px;color:var(--dsw-alias-label-secondary);pointer-events:none;border-radius:6px;padding:6px 10px;font-size:12px;line-height:16px;position:fixed;box-shadow:0 2px 10px #0000002e}.TWf_pa_cascade{z-index:20;pointer-events:none;flex-direction:column;align-items:flex-start;gap:6px;display:flex;position:fixed}.TWf_pa_card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);width:380px;color:var(--dsw-alias-label-secondary);white-space:normal;word-break:break-word;pointer-events:auto;cursor:pointer;border-radius:10px;padding:8px 12px 10px;font-size:13px;line-height:18px;transition:border-color .12s,box-shadow .12s;box-shadow:0 1px 6px #0000001a}.TWf_pa_card:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary);box-shadow:0 3px 12px #0000002e}.TWf_pa_cardSelected{border-color:var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);box-shadow:inset 3px 0 0 0 var(--dsw-alias-brand-primary), 0 6px 20px #00000038}.TWf_pa_cardSelected:hover{border-color:var(--dsw-alias-brand-primary);box-shadow:inset 3px 0 0 0 var(--dsw-alias-brand-primary), 0 6px 22px #00000042}.TWf_pa_cardTitle{color:var(--dsw-alias-label-tertiary);margin-bottom:4px;font-size:11px;font-weight:600}.TWf_pa_cardBody{scrollbar-width:thin;max-height:96px;overflow-y:auto}.TWf_pa_cardClamp{word-break:break-word;-webkit-box-orient:vertical;font-size:13px;line-height:18px;display:-webkit-box;overflow:hidden}.TWf_pa_cardLine+.TWf_pa_cardLine{border-top:1px solid var(--dsw-alias-border-l1);margin-top:6px;padding-top:6px}.TWf_pa_cardTime{color:var(--dsw-alias-label-tertiary);margin-top:6px;font-size:11px}.TWf_pa_settings{flex-direction:column;gap:8px;padding:4px 0;display:flex}.TWf_pa_settingsTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:600}.TWf_pa_settingsDesc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}.TWf_pa_segmented{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;align-self:flex-start;display:inline-flex;overflow:hidden}.TWf_pa_segment{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:8px 16px;font-size:13px;line-height:1;transition:background .12s,color .12s}.TWf_pa_segment+.TWf_pa_segment{border-left:1px solid var(--dsw-alias-border-l1)}.TWf_pa_segment:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.TWf_pa_segmentActive,.TWf_pa_segmentActive:hover{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-inverse)}";
189
+ const css = ".TWf_pa_rail{z-index:1;box-sizing:border-box;pointer-events:none;background:0 0;flex-direction:column;align-items:center;width:44px;display:flex;position:absolute;top:0;bottom:0;left:0}.TWf_pa_railRight{left:auto}.TWf_pa_queue{box-sizing:border-box;pointer-events:none;flex-direction:column;flex:0 auto;align-items:center;gap:6px;width:100%;min-height:0;max-height:60%;margin:auto 0;display:flex}.TWf_pa_list{scrollbar-width:none;pointer-events:auto;flex-direction:column;flex:auto;align-items:center;gap:6px;width:100%;min-height:0;padding:8px 0;display:flex;overflow:hidden auto}.TWf_pa_list::-webkit-scrollbar{width:0;height:0;display:none}.TWf_pa_dot{pointer-events:auto;background:var(--dsw-alias-border-l3);cursor:pointer;border:none;border-radius:50%;flex:none;width:8px;height:8px;padding:0;transition:transform .12s,background .12s}.TWf_pa_dot:hover,.TWf_pa_dot.TWf_pa_focused,.TWf_pa_dot.TWf_pa_active{background:var(--dsw-alias-brand-primary)}@keyframes TWf_pa_dotEnterFromBottom{0%{opacity:0;transform:translateY(7px)scale(.4)}60%{opacity:1}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes TWf_pa_dotEnterFromTop{0%{opacity:0;transform:translateY(-7px)scale(.4)}60%{opacity:1}to{opacity:1;transform:translateY(0)scale(1)}}.TWf_pa_dotEnterFromBottom{animation:.32s cubic-bezier(.2,.9,.3,1.2) both TWf_pa_dotEnterFromBottom}.TWf_pa_dotEnterFromTop{animation:.32s cubic-bezier(.2,.9,.3,1.2) both TWf_pa_dotEnterFromTop}@media (prefers-reduced-motion:reduce){.TWf_pa_dotEnterFromBottom,.TWf_pa_dotEnterFromTop{animation:none}}.TWf_pa_count{color:var(--dsw-alias-label-tertiary);user-select:none;flex:none;font-size:10px;font-weight:600;line-height:1}.TWf_pa_dots{flex-direction:column;align-items:center;gap:6px;display:flex}.TWf_pa_navBtn{pointer-events:auto;cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;width:14px;height:12px;padding:0;display:flex}.TWf_pa_navBtn:before{content:\"\";border-left:5px solid #0000;border-right:5px solid #0000;width:0;height:0;transition:border-color .12s;display:block}.TWf_pa_navUp:before{border-bottom:7px solid var(--dsw-alias-border-l3)}.TWf_pa_navDown:before{border-top:7px solid var(--dsw-alias-border-l3)}.TWf_pa_navUp:hover:before{border-bottom-color:var(--dsw-alias-brand-primary)}.TWf_pa_navDown:hover:before{border-top-color:var(--dsw-alias-brand-primary)}.TWf_pa_empty{color:var(--dsw-alias-label-tertiary);text-align:center;word-break:break-word;padding:10px 4px;font-size:11px}.TWf_pa_hint{z-index:30;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);max-width:260px;color:var(--dsw-alias-label-secondary);pointer-events:none;border-radius:6px;padding:6px 10px;font-size:12px;line-height:16px;position:fixed;box-shadow:0 2px 10px #0000002e}.TWf_pa_cascade{z-index:20;pointer-events:none;flex-direction:column;align-items:flex-start;gap:6px;display:flex;position:fixed}.TWf_pa_cascadeRight{align-items:flex-end}.TWf_pa_card{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);width:380px;color:var(--dsw-alias-label-secondary);white-space:normal;word-break:break-word;pointer-events:auto;cursor:pointer;border-radius:10px;padding:8px 12px 10px;font-size:13px;line-height:18px;transition:border-color .12s,box-shadow .12s;box-shadow:0 1px 6px #0000001a}.TWf_pa_card:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary);box-shadow:0 3px 12px #0000002e}.TWf_pa_cardSelected{border-color:var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);box-shadow:inset 3px 0 0 0 var(--dsw-alias-brand-primary), 0 6px 20px #00000038}.TWf_pa_cardSelected:hover{border-color:var(--dsw-alias-brand-primary);box-shadow:inset 3px 0 0 0 var(--dsw-alias-brand-primary), 0 6px 22px #00000042}.TWf_pa_cardSelectedRight{box-shadow:inset -3px 0 0 0 var(--dsw-alias-brand-primary), 0 6px 20px #00000038}.TWf_pa_cardSelectedRight:hover{box-shadow:inset -3px 0 0 0 var(--dsw-alias-brand-primary), 0 6px 22px #00000042}.TWf_pa_cardTitle{color:var(--dsw-alias-label-tertiary);margin-bottom:4px;font-size:11px;font-weight:600}.TWf_pa_cardBody{--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);max-height:96px;overflow-y:auto}.TWf_pa_cardClamp{word-break:break-word;-webkit-box-orient:vertical;font-size:13px;line-height:18px;display:-webkit-box;overflow:hidden}.TWf_pa_cardLine+.TWf_pa_cardLine{border-top:1px solid var(--dsw-alias-border-l1);margin-top:6px;padding-top:6px}.TWf_pa_cardTime{color:var(--dsw-alias-label-tertiary);margin-top:6px;font-size:11px}.TWf_pa_settings{flex-direction:column;gap:8px;padding:4px 0;display:flex}.TWf_pa_settingsTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:600}.TWf_pa_segmented+.TWf_pa_settingsTitle{margin-top:12px}.TWf_pa_settingsDesc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}.TWf_pa_segmented{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;align-self:flex-start;display:inline-flex;overflow:hidden}.TWf_pa_segment{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:8px 16px;font-size:13px;line-height:1;transition:background .12s,color .12s}.TWf_pa_segment+.TWf_pa_segment{border-left:1px solid var(--dsw-alias-border-l1)}.TWf_pa_segment:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.TWf_pa_segmentActive,.TWf_pa_segmentActive:hover{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-inverse)}";
190
190
  const tagId = "@luziyang2026/dsh-question-nav/question-nav.module.css";
191
191
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
192
192
  const tag = document.createElement("style");
@@ -202,17 +202,20 @@ window.__ModuleLoader__.load({
202
202
  "cardClamp": "TWf_pa_cardClamp",
203
203
  "cardLine": "TWf_pa_cardLine",
204
204
  "cardSelected": "TWf_pa_cardSelected",
205
+ "cardSelectedRight": "TWf_pa_cardSelectedRight",
205
206
  "cardTime": "TWf_pa_cardTime",
206
207
  "cardTitle": "TWf_pa_cardTitle",
207
208
  "cascade": "TWf_pa_cascade",
209
+ "cascadeRight": "TWf_pa_cascadeRight",
208
210
  "count": "TWf_pa_count",
209
211
  "dot": "TWf_pa_dot",
212
+ "dotEnterFromBottom": "TWf_pa_dotEnterFromBottom",
213
+ "dotEnterFromTop": "TWf_pa_dotEnterFromTop",
210
214
  "dots": "TWf_pa_dots",
211
215
  "empty": "TWf_pa_empty",
212
216
  "focused": "TWf_pa_focused",
213
217
  "hint": "TWf_pa_hint",
214
218
  "list": "TWf_pa_list",
215
- "listScrollable": "TWf_pa_listScrollable",
216
219
  "navBtn": "TWf_pa_navBtn",
217
220
  "navDown": "TWf_pa_navDown",
218
221
  "navUp": "TWf_pa_navUp",
@@ -246,9 +249,12 @@ window.__ModuleLoader__.load({
246
249
  * the band — the dot column stays put while you browse.
247
250
  *
248
251
  * Overflow is paged, not auto-scrolled: two small triangle buttons in the dot
249
- * style sit above and below the dot queue (▲ / ▼), each click revealing five
250
- * hidden dots. The native scrollbar stays hidden and the column fades at its
251
- * edges as a pure visual cue no hover auto-scroll of any kind.
252
+ * style sit above and below the dot queue (▲ / ▼), each click revealing one
253
+ * page of hidden dots (the page size is configurable in the plugin settings,
254
+ * default 5) with a staggered pop-in animation as click feedback. The native
255
+ * scrollbar stays hidden; the triangles themselves are the overflow cue (each
256
+ * appears only while its direction has more to reveal) — no hover auto-scroll
257
+ * of any kind.
252
258
  *
253
259
  * Data source: the host-folded `questionIndex` session projection (whole
254
260
  * history, persisted host-side, pushed live through session/projection
@@ -298,6 +304,7 @@ window.__ModuleLoader__.load({
298
304
  const [hint, setHint] = (0, react.useState)(null);
299
305
  const [focus, setFocus] = (0, react.useState)(null);
300
306
  const [align, setAlign] = (0, react.useState)(() => props.align());
307
+ const [pageSize, setPageSize] = (0, react.useState)(() => props.pageSize());
301
308
  const panelRef = (0, react.useRef)(null);
302
309
  const listRef = (0, react.useRef)(null);
303
310
  const hintTimerRef = (0, react.useRef)(null);
@@ -308,6 +315,8 @@ window.__ModuleLoader__.load({
308
315
  top: 0,
309
316
  max: 0
310
317
  });
318
+ const [revealed, setRevealed] = (0, react.useState)(null);
319
+ const revealTimerRef = (0, react.useRef)(null);
311
320
  const syncScroll = () => {
312
321
  const list = listRef.current;
313
322
  if (list === null) return;
@@ -321,8 +330,33 @@ window.__ModuleLoader__.load({
321
330
  const list = listRef.current;
322
331
  if (list === null) return;
323
332
  const max = Math.max(0, list.scrollHeight - list.clientHeight);
324
- list.scrollTop = clampScrollTop(list.scrollTop + dir * pageStep(), max);
333
+ const target = clampScrollTop(list.scrollTop + dir * pageStep(8, 6, pageSize), max);
334
+ if (target === list.scrollTop) return;
335
+ const band = list.getBoundingClientRect();
336
+ const els = Array.from(list.querySelectorAll("[data-question-nav-key]"));
337
+ const wasVisible = els.map((el) => {
338
+ const r = el.getBoundingClientRect();
339
+ return r.bottom > band.top && r.top < band.bottom;
340
+ });
341
+ list.scrollTop = target;
325
342
  syncScroll();
343
+ const next = /* @__PURE__ */ new Map();
344
+ const fresh = [];
345
+ els.forEach((el, i) => {
346
+ if (wasVisible[i]) return;
347
+ const r = el.getBoundingClientRect();
348
+ const key = el.dataset.questionNavKey;
349
+ if (key !== void 0 && r.bottom > band.top && r.top < band.bottom) fresh.push(key);
350
+ });
351
+ if (dir === 1) fresh.reverse();
352
+ fresh.forEach((key, order) => next.set(key, {
353
+ dir,
354
+ order
355
+ }));
356
+ if (next.size === 0) return;
357
+ setRevealed(next);
358
+ if (revealTimerRef.current !== null) window.clearTimeout(revealTimerRef.current);
359
+ revealTimerRef.current = window.setTimeout(() => setRevealed(null), 320 + next.size * 35 + 120);
326
360
  };
327
361
  const canPageUp = scrollable && scrollPos.top > 0;
328
362
  const canPageDown = scrollable && scrollPos.top < scrollPos.max;
@@ -336,7 +370,10 @@ window.__ModuleLoader__.load({
336
370
  cancelClearFocus();
337
371
  clearFocusTimerRef.current = window.setTimeout(() => setFocus(null), 240);
338
372
  };
339
- (0, react.useEffect)(() => props.subscribeAlign(() => setAlign(props.align())), [props]);
373
+ (0, react.useEffect)(() => props.subscribeSettings(() => {
374
+ setAlign(props.align());
375
+ setPageSize(props.pageSize());
376
+ }), [props]);
340
377
  const showHint = (message) => {
341
378
  setHint(message);
342
379
  if (hintTimerRef.current !== null) window.clearTimeout(hintTimerRef.current);
@@ -476,6 +513,7 @@ window.__ModuleLoader__.load({
476
513
  (0, react.useEffect)(() => () => {
477
514
  if (hintTimerRef.current !== null) window.clearTimeout(hintTimerRef.current);
478
515
  if (clearFocusTimerRef.current !== null) window.clearTimeout(clearFocusTimerRef.current);
516
+ if (revealTimerRef.current !== null) window.clearTimeout(revealTimerRef.current);
479
517
  }, []);
480
518
  if (!visible) return null;
481
519
  const onJump = (dot) => {
@@ -532,7 +570,7 @@ window.__ModuleLoader__.load({
532
570
  }) : null,
533
571
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
534
572
  ref: listRef,
535
- className: scrollable ? `${question_nav_module_css_default.list} ${question_nav_module_css_default.listScrollable}` : question_nav_module_css_default.list,
573
+ className: question_nav_module_css_default.list,
536
574
  onScroll: syncScroll,
537
575
  onMouseLeave: scheduleClearFocus,
538
576
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -541,14 +579,20 @@ window.__ModuleLoader__.load({
541
579
  const isFocused = focus !== null && focus.key === dot.key;
542
580
  const tier = selectedIndex < 0 ? null : focusTier(index - selectedIndex);
543
581
  const scale = jumpingKey === dot.key ? 1.6 : tier !== null ? focusScale(tier) : 1;
582
+ const reveal = revealed?.get(dot.key);
544
583
  const cls = [question_nav_module_css_default.dot];
545
584
  if (isFocused) cls.push(question_nav_module_css_default.focused);
546
585
  if (jumpingKey === dot.key) cls.push(question_nav_module_css_default.active);
586
+ if (reveal !== void 0) cls.push(reveal.dir === 1 ? question_nav_module_css_default.dotEnterFromBottom : question_nav_module_css_default.dotEnterFromTop);
547
587
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
548
588
  className: cls.join(" "),
549
- style: { transform: `scale(${scale})` },
589
+ style: {
590
+ transform: `scale(${scale})`,
591
+ ...reveal !== void 0 ? { animationDelay: `${reveal.order * 35}ms` } : {}
592
+ },
550
593
  "data-question-nav-focused": isFocused ? "true" : void 0,
551
594
  "data-question-nav-index": index,
595
+ "data-question-nav-key": dot.key,
552
596
  "aria-label": dot.texts[0] ?? "",
553
597
  onMouseEnter: (e) => openFocus(dot, e.currentTarget),
554
598
  onClick: () => onJump(dot)
@@ -579,7 +623,7 @@ window.__ModuleLoader__.load({
579
623
  children: hint
580
624
  }), document.body) : null,
581
625
  focus !== null ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
582
- className: question_nav_module_css_default.cascade,
626
+ className: align === "right" ? `${question_nav_module_css_default.cascade} ${question_nav_module_css_default.cascadeRight}` : question_nav_module_css_default.cascade,
583
627
  style: {
584
628
  ...focus.left !== void 0 ? { left: focus.left } : {},
585
629
  ...focus.right !== void 0 ? { right: focus.right } : {},
@@ -593,7 +637,7 @@ window.__ModuleLoader__.load({
593
637
  if (metrics === null) return null;
594
638
  const isSelected = item.distance === 0;
595
639
  const text = item.dot.texts.join(" · ");
596
- const cls = isSelected ? `${question_nav_module_css_default.card} ${question_nav_module_css_default.cardSelected}` : question_nav_module_css_default.card;
640
+ const cls = isSelected ? `${question_nav_module_css_default.card} ${question_nav_module_css_default.cardSelected}${align === "right" ? ` ${question_nav_module_css_default.cardSelectedRight}` : ""}` : question_nav_module_css_default.card;
597
641
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
598
642
  className: cls,
599
643
  role: "button",
@@ -646,24 +690,45 @@ window.__ModuleLoader__.load({
646
690
  /** Field carrying the selected anchor edge. */
647
691
  const ALIGN_FIELD = "align";
648
692
  //#endregion
693
+ //#region src/core/page-size.ts
694
+ /**
695
+ * Page-size constants for the ▲/▼ paging buttons, shared by the host schema
696
+ * and the browser settings scope. Pure data: no DSH imports, so the client
697
+ * bundle may inline this module (a Host import here would leak into the
698
+ * browser half).
699
+ *
700
+ * @module dsh-question-nav/page-size
701
+ */
702
+ /** Selectable page sizes: how many hidden dots one ▲/▼ click reveals. */
703
+ const PAGE_SIZE_OPTIONS = [
704
+ 3,
705
+ 5,
706
+ 8,
707
+ 10
708
+ ];
709
+ /** Field carrying the selected page size. */
710
+ const PAGE_SIZE_FIELD = "pageSize";
711
+ //#endregion
649
712
  //#region src/client/QuestionNavSettingsTab.tsx
650
713
  /**
651
714
  * The plugin's settings page inside the shell's Plugins section
652
- * (`settings.plugins.tab`): a segmented control choosing which edge of the
653
- * conversation column the dot rail anchors to. The choice is written to the
715
+ * (`settings.plugins.tab`): two segmented controls which edge of the
716
+ * conversation column the dot rail anchors to, and how many hidden dots one
717
+ * ▲/▼ paging-button click reveals. The choices are written to the
654
718
  * `question-nav` settings namespace (registered by the host half); the strip
655
- * re-anchors live when the snapshot changes.
719
+ * re-anchors and re-steps live when the snapshot changes.
656
720
  *
657
721
  * @module dsh-question-nav/client/settings-tab
658
722
  */
659
723
  /** Re-render on settings snapshot changes (the register inject face is static). */
660
- function useAlignTick(subscribe) {
724
+ function useSettingsTick(subscribe) {
661
725
  const [, bump] = (0, react.useState)(0);
662
726
  (0, react.useEffect)(() => subscribe(() => bump((n) => n + 1)), [subscribe]);
663
727
  }
664
728
  function QuestionNavSettingsTab(props) {
665
- useAlignTick(props.subscribeAlign);
729
+ useSettingsTick(props.subscribeSettings);
666
730
  const align = props.align();
731
+ const pageSize = props.pageSize();
667
732
  const t = props.t;
668
733
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
669
734
  className: question_nav_module_css_default.settings,
@@ -688,16 +753,41 @@ window.__ModuleLoader__.load({
688
753
  onClick: () => props.setAlign(option),
689
754
  children: t(option === "left" ? "settings.align.left" : "settings.align.right")
690
755
  }, option))
756
+ }),
757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
758
+ className: question_nav_module_css_default.settingsTitle,
759
+ children: t("settings.pagesize.title")
760
+ }),
761
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
762
+ className: question_nav_module_css_default.settingsDesc,
763
+ children: t("settings.pagesize.desc")
764
+ }),
765
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
766
+ className: question_nav_module_css_default.segmented,
767
+ role: "radiogroup",
768
+ "aria-label": t("settings.pagesize.title"),
769
+ children: PAGE_SIZE_OPTIONS.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
770
+ type: "button",
771
+ role: "radio",
772
+ "aria-checked": pageSize === option,
773
+ className: pageSize === option ? `${question_nav_module_css_default.segment} ${question_nav_module_css_default.segmentActive}` : question_nav_module_css_default.segment,
774
+ onClick: () => props.setPageSize(option),
775
+ children: option
776
+ }, option))
691
777
  })
692
778
  ]
693
779
  });
694
780
  }
695
781
  //#endregion
696
782
  //#region src/client/settings.ts
697
- /** Narrow a raw section to the anchor field. */
783
+ /** Narrow a raw section value to the anchor field. */
698
784
  function isAlignPreference(value) {
699
785
  return ALIGN_OPTIONS.some((option) => option === value);
700
786
  }
787
+ /** Narrow a raw section value to the page-size field. */
788
+ function isPageSize(value) {
789
+ return PAGE_SIZE_OPTIONS.includes(value);
790
+ }
701
791
  /** Reactive handle over the plugin's durable settings section. */
702
792
  var QuestionNavSettingsController = class {
703
793
  scope;
@@ -705,6 +795,7 @@ window.__ModuleLoader__.load({
705
795
  unsubscribe = () => {};
706
796
  state = {
707
797
  align: DEFAULT_ALIGN,
798
+ pageSize: 5,
708
799
  overridden: false
709
800
  };
710
801
  /**
@@ -721,7 +812,7 @@ window.__ModuleLoader__.load({
721
812
  this.unsubscribe = this.scope.subscribe(() => {
722
813
  if (this.scope === void 0) return;
723
814
  const next = this.derive(this.scope.getSnapshot());
724
- if (next.align === this.state.align && next.overridden === this.state.overridden) return;
815
+ if (next.align === this.state.align && next.pageSize === this.state.pageSize && next.overridden === this.state.overridden) return;
725
816
  this.state = next;
726
817
  for (const listener of this.listeners) listener();
727
818
  });
@@ -730,7 +821,8 @@ window.__ModuleLoader__.load({
730
821
  const user = snapshot.user;
731
822
  return {
732
823
  align: snapshot.status === "ready" && isAlignPreference(snapshot.value?.align) ? snapshot.value.align : DEFAULT_ALIGN,
733
- overridden: user !== void 0 && user.align !== void 0
824
+ pageSize: snapshot.status === "ready" && isPageSize(snapshot.value?.pageSize) ? snapshot.value.pageSize : 5,
825
+ overridden: user !== void 0 && (user.align !== void 0 || user.pageSize !== void 0)
734
826
  };
735
827
  }
736
828
  /** Release the scope subscription (bound on the settings fiber's lifecycle). */
@@ -754,6 +846,11 @@ window.__ModuleLoader__.load({
754
846
  if (this.scope === void 0) return;
755
847
  this.scope.set(ALIGN_FIELD, align);
756
848
  }
849
+ /** Route the user's page-size choice to the Host document. */
850
+ setPageSize(pageSize) {
851
+ if (this.scope === void 0) return;
852
+ this.scope.set(PAGE_SIZE_FIELD, pageSize);
853
+ }
757
854
  };
758
855
  //#endregion
759
856
  //#region src/client/locales.ts
@@ -763,8 +860,8 @@ window.__ModuleLoader__.load({
763
860
  */
764
861
  const zh = {
765
862
  "strip.empty": "本会话还没有提问",
766
- "strip.up": "向上翻出 5 个提问圆点",
767
- "strip.down": "向下翻出 5 个提问圆点",
863
+ "strip.up": "向上翻出更多提问圆点",
864
+ "strip.down": "向下翻出更多提问圆点",
768
865
  "jump.inactive": "聊天视图未激活",
769
866
  "jump.hidden": "目标无独立气泡,已定位到邻近内容",
770
867
  "jump.notfound": "目标未加载或不存在(可能已压缩)",
@@ -773,12 +870,14 @@ window.__ModuleLoader__.load({
773
870
  "settings.align.title": "导航条对齐",
774
871
  "settings.align.desc": "选择圆点导航条锚定在对话栏的哪一侧。",
775
872
  "settings.align.left": "左侧",
776
- "settings.align.right": "右侧"
873
+ "settings.align.right": "右侧",
874
+ "settings.pagesize.title": "每次翻出数量",
875
+ "settings.pagesize.desc": "点击 ▲/▼ 三角按钮时,每次翻出的隐藏圆点数量。"
777
876
  };
778
877
  const en = {
779
878
  "strip.empty": "No questions in this session yet",
780
- "strip.up": "Reveal 5 question dots above",
781
- "strip.down": "Reveal 5 question dots below",
879
+ "strip.up": "Reveal more question dots above",
880
+ "strip.down": "Reveal more question dots below",
782
881
  "jump.inactive": "Chat view is not active",
783
882
  "jump.hidden": "No dedicated bubble; landed on nearby content",
784
883
  "jump.notfound": "Target not loaded or missing (maybe compacted)",
@@ -787,7 +886,9 @@ window.__ModuleLoader__.load({
787
886
  "settings.align.title": "Rail alignment",
788
887
  "settings.align.desc": "Choose which edge of the conversation column the dot rail anchors to.",
789
888
  "settings.align.left": "Left",
790
- "settings.align.right": "Right"
889
+ "settings.align.right": "Right",
890
+ "settings.pagesize.title": "Dots per page",
891
+ "settings.pagesize.desc": "How many hidden dots one click of the ▲/▼ paging buttons reveals."
791
892
  };
792
893
  //#endregion
793
894
  //#region src/core/nodes.ts
@@ -1017,8 +1118,10 @@ window.__ModuleLoader__.load({
1017
1118
  jumpToQuestion(ports, key);
1018
1119
  },
1019
1120
  align: () => settings.getSnapshot().align,
1020
- subscribeAlign: (cb) => settings.subscribe(cb),
1021
- setAlign: (align) => settings.setAlign(align)
1121
+ subscribeSettings: (cb) => settings.subscribe(cb),
1122
+ setAlign: (align) => settings.setAlign(align),
1123
+ pageSize: () => settings.getSnapshot().pageSize,
1124
+ setPageSize: (pageSize) => settings.setPageSize(pageSize)
1022
1125
  };
1023
1126
  }
1024
1127
  /**