@luziyang2026/dsh-question-nav 0.7.3 → 0.7.4

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_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_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_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");
@@ -207,12 +207,13 @@ window.__ModuleLoader__.load({
207
207
  "cascade": "TWf_pa_cascade",
208
208
  "count": "TWf_pa_count",
209
209
  "dot": "TWf_pa_dot",
210
+ "dotEnterFromBottom": "TWf_pa_dotEnterFromBottom",
211
+ "dotEnterFromTop": "TWf_pa_dotEnterFromTop",
210
212
  "dots": "TWf_pa_dots",
211
213
  "empty": "TWf_pa_empty",
212
214
  "focused": "TWf_pa_focused",
213
215
  "hint": "TWf_pa_hint",
214
216
  "list": "TWf_pa_list",
215
- "listScrollable": "TWf_pa_listScrollable",
216
217
  "navBtn": "TWf_pa_navBtn",
217
218
  "navDown": "TWf_pa_navDown",
218
219
  "navUp": "TWf_pa_navUp",
@@ -246,9 +247,12 @@ window.__ModuleLoader__.load({
246
247
  * the band — the dot column stays put while you browse.
247
248
  *
248
249
  * 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.
250
+ * style sit above and below the dot queue (▲ / ▼), each click revealing one
251
+ * page of hidden dots (the page size is configurable in the plugin settings,
252
+ * default 5) with a staggered pop-in animation as click feedback. The native
253
+ * scrollbar stays hidden; the triangles themselves are the overflow cue (each
254
+ * appears only while its direction has more to reveal) — no hover auto-scroll
255
+ * of any kind.
252
256
  *
253
257
  * Data source: the host-folded `questionIndex` session projection (whole
254
258
  * history, persisted host-side, pushed live through session/projection
@@ -298,6 +302,7 @@ window.__ModuleLoader__.load({
298
302
  const [hint, setHint] = (0, react.useState)(null);
299
303
  const [focus, setFocus] = (0, react.useState)(null);
300
304
  const [align, setAlign] = (0, react.useState)(() => props.align());
305
+ const [pageSize, setPageSize] = (0, react.useState)(() => props.pageSize());
301
306
  const panelRef = (0, react.useRef)(null);
302
307
  const listRef = (0, react.useRef)(null);
303
308
  const hintTimerRef = (0, react.useRef)(null);
@@ -308,6 +313,8 @@ window.__ModuleLoader__.load({
308
313
  top: 0,
309
314
  max: 0
310
315
  });
316
+ const [revealed, setRevealed] = (0, react.useState)(null);
317
+ const revealTimerRef = (0, react.useRef)(null);
311
318
  const syncScroll = () => {
312
319
  const list = listRef.current;
313
320
  if (list === null) return;
@@ -321,8 +328,33 @@ window.__ModuleLoader__.load({
321
328
  const list = listRef.current;
322
329
  if (list === null) return;
323
330
  const max = Math.max(0, list.scrollHeight - list.clientHeight);
324
- list.scrollTop = clampScrollTop(list.scrollTop + dir * pageStep(), max);
331
+ const target = clampScrollTop(list.scrollTop + dir * pageStep(8, 6, pageSize), max);
332
+ if (target === list.scrollTop) return;
333
+ const band = list.getBoundingClientRect();
334
+ const els = Array.from(list.querySelectorAll("[data-question-nav-key]"));
335
+ const wasVisible = els.map((el) => {
336
+ const r = el.getBoundingClientRect();
337
+ return r.bottom > band.top && r.top < band.bottom;
338
+ });
339
+ list.scrollTop = target;
325
340
  syncScroll();
341
+ const next = /* @__PURE__ */ new Map();
342
+ const fresh = [];
343
+ els.forEach((el, i) => {
344
+ if (wasVisible[i]) return;
345
+ const r = el.getBoundingClientRect();
346
+ const key = el.dataset.questionNavKey;
347
+ if (key !== void 0 && r.bottom > band.top && r.top < band.bottom) fresh.push(key);
348
+ });
349
+ if (dir === 1) fresh.reverse();
350
+ fresh.forEach((key, order) => next.set(key, {
351
+ dir,
352
+ order
353
+ }));
354
+ if (next.size === 0) return;
355
+ setRevealed(next);
356
+ if (revealTimerRef.current !== null) window.clearTimeout(revealTimerRef.current);
357
+ revealTimerRef.current = window.setTimeout(() => setRevealed(null), 320 + next.size * 35 + 120);
326
358
  };
327
359
  const canPageUp = scrollable && scrollPos.top > 0;
328
360
  const canPageDown = scrollable && scrollPos.top < scrollPos.max;
@@ -336,7 +368,10 @@ window.__ModuleLoader__.load({
336
368
  cancelClearFocus();
337
369
  clearFocusTimerRef.current = window.setTimeout(() => setFocus(null), 240);
338
370
  };
339
- (0, react.useEffect)(() => props.subscribeAlign(() => setAlign(props.align())), [props]);
371
+ (0, react.useEffect)(() => props.subscribeSettings(() => {
372
+ setAlign(props.align());
373
+ setPageSize(props.pageSize());
374
+ }), [props]);
340
375
  const showHint = (message) => {
341
376
  setHint(message);
342
377
  if (hintTimerRef.current !== null) window.clearTimeout(hintTimerRef.current);
@@ -476,6 +511,7 @@ window.__ModuleLoader__.load({
476
511
  (0, react.useEffect)(() => () => {
477
512
  if (hintTimerRef.current !== null) window.clearTimeout(hintTimerRef.current);
478
513
  if (clearFocusTimerRef.current !== null) window.clearTimeout(clearFocusTimerRef.current);
514
+ if (revealTimerRef.current !== null) window.clearTimeout(revealTimerRef.current);
479
515
  }, []);
480
516
  if (!visible) return null;
481
517
  const onJump = (dot) => {
@@ -532,7 +568,7 @@ window.__ModuleLoader__.load({
532
568
  }) : null,
533
569
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
534
570
  ref: listRef,
535
- className: scrollable ? `${question_nav_module_css_default.list} ${question_nav_module_css_default.listScrollable}` : question_nav_module_css_default.list,
571
+ className: question_nav_module_css_default.list,
536
572
  onScroll: syncScroll,
537
573
  onMouseLeave: scheduleClearFocus,
538
574
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -541,14 +577,20 @@ window.__ModuleLoader__.load({
541
577
  const isFocused = focus !== null && focus.key === dot.key;
542
578
  const tier = selectedIndex < 0 ? null : focusTier(index - selectedIndex);
543
579
  const scale = jumpingKey === dot.key ? 1.6 : tier !== null ? focusScale(tier) : 1;
580
+ const reveal = revealed?.get(dot.key);
544
581
  const cls = [question_nav_module_css_default.dot];
545
582
  if (isFocused) cls.push(question_nav_module_css_default.focused);
546
583
  if (jumpingKey === dot.key) cls.push(question_nav_module_css_default.active);
584
+ if (reveal !== void 0) cls.push(reveal.dir === 1 ? question_nav_module_css_default.dotEnterFromBottom : question_nav_module_css_default.dotEnterFromTop);
547
585
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
548
586
  className: cls.join(" "),
549
- style: { transform: `scale(${scale})` },
587
+ style: {
588
+ transform: `scale(${scale})`,
589
+ ...reveal !== void 0 ? { animationDelay: `${reveal.order * 35}ms` } : {}
590
+ },
550
591
  "data-question-nav-focused": isFocused ? "true" : void 0,
551
592
  "data-question-nav-index": index,
593
+ "data-question-nav-key": dot.key,
552
594
  "aria-label": dot.texts[0] ?? "",
553
595
  onMouseEnter: (e) => openFocus(dot, e.currentTarget),
554
596
  onClick: () => onJump(dot)
@@ -646,24 +688,45 @@ window.__ModuleLoader__.load({
646
688
  /** Field carrying the selected anchor edge. */
647
689
  const ALIGN_FIELD = "align";
648
690
  //#endregion
691
+ //#region src/core/page-size.ts
692
+ /**
693
+ * Page-size constants for the ▲/▼ paging buttons, shared by the host schema
694
+ * and the browser settings scope. Pure data: no DSH imports, so the client
695
+ * bundle may inline this module (a Host import here would leak into the
696
+ * browser half).
697
+ *
698
+ * @module dsh-question-nav/page-size
699
+ */
700
+ /** Selectable page sizes: how many hidden dots one ▲/▼ click reveals. */
701
+ const PAGE_SIZE_OPTIONS = [
702
+ 3,
703
+ 5,
704
+ 8,
705
+ 10
706
+ ];
707
+ /** Field carrying the selected page size. */
708
+ const PAGE_SIZE_FIELD = "pageSize";
709
+ //#endregion
649
710
  //#region src/client/QuestionNavSettingsTab.tsx
650
711
  /**
651
712
  * 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
713
+ * (`settings.plugins.tab`): two segmented controls which edge of the
714
+ * conversation column the dot rail anchors to, and how many hidden dots one
715
+ * ▲/▼ paging-button click reveals. The choices are written to the
654
716
  * `question-nav` settings namespace (registered by the host half); the strip
655
- * re-anchors live when the snapshot changes.
717
+ * re-anchors and re-steps live when the snapshot changes.
656
718
  *
657
719
  * @module dsh-question-nav/client/settings-tab
658
720
  */
659
721
  /** Re-render on settings snapshot changes (the register inject face is static). */
660
- function useAlignTick(subscribe) {
722
+ function useSettingsTick(subscribe) {
661
723
  const [, bump] = (0, react.useState)(0);
662
724
  (0, react.useEffect)(() => subscribe(() => bump((n) => n + 1)), [subscribe]);
663
725
  }
664
726
  function QuestionNavSettingsTab(props) {
665
- useAlignTick(props.subscribeAlign);
727
+ useSettingsTick(props.subscribeSettings);
666
728
  const align = props.align();
729
+ const pageSize = props.pageSize();
667
730
  const t = props.t;
668
731
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
669
732
  className: question_nav_module_css_default.settings,
@@ -688,16 +751,41 @@ window.__ModuleLoader__.load({
688
751
  onClick: () => props.setAlign(option),
689
752
  children: t(option === "left" ? "settings.align.left" : "settings.align.right")
690
753
  }, option))
754
+ }),
755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
756
+ className: question_nav_module_css_default.settingsTitle,
757
+ children: t("settings.pagesize.title")
758
+ }),
759
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
760
+ className: question_nav_module_css_default.settingsDesc,
761
+ children: t("settings.pagesize.desc")
762
+ }),
763
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
764
+ className: question_nav_module_css_default.segmented,
765
+ role: "radiogroup",
766
+ "aria-label": t("settings.pagesize.title"),
767
+ children: PAGE_SIZE_OPTIONS.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
768
+ type: "button",
769
+ role: "radio",
770
+ "aria-checked": pageSize === option,
771
+ className: pageSize === option ? `${question_nav_module_css_default.segment} ${question_nav_module_css_default.segmentActive}` : question_nav_module_css_default.segment,
772
+ onClick: () => props.setPageSize(option),
773
+ children: option
774
+ }, option))
691
775
  })
692
776
  ]
693
777
  });
694
778
  }
695
779
  //#endregion
696
780
  //#region src/client/settings.ts
697
- /** Narrow a raw section to the anchor field. */
781
+ /** Narrow a raw section value to the anchor field. */
698
782
  function isAlignPreference(value) {
699
783
  return ALIGN_OPTIONS.some((option) => option === value);
700
784
  }
785
+ /** Narrow a raw section value to the page-size field. */
786
+ function isPageSize(value) {
787
+ return PAGE_SIZE_OPTIONS.includes(value);
788
+ }
701
789
  /** Reactive handle over the plugin's durable settings section. */
702
790
  var QuestionNavSettingsController = class {
703
791
  scope;
@@ -705,6 +793,7 @@ window.__ModuleLoader__.load({
705
793
  unsubscribe = () => {};
706
794
  state = {
707
795
  align: DEFAULT_ALIGN,
796
+ pageSize: 5,
708
797
  overridden: false
709
798
  };
710
799
  /**
@@ -721,7 +810,7 @@ window.__ModuleLoader__.load({
721
810
  this.unsubscribe = this.scope.subscribe(() => {
722
811
  if (this.scope === void 0) return;
723
812
  const next = this.derive(this.scope.getSnapshot());
724
- if (next.align === this.state.align && next.overridden === this.state.overridden) return;
813
+ if (next.align === this.state.align && next.pageSize === this.state.pageSize && next.overridden === this.state.overridden) return;
725
814
  this.state = next;
726
815
  for (const listener of this.listeners) listener();
727
816
  });
@@ -730,7 +819,8 @@ window.__ModuleLoader__.load({
730
819
  const user = snapshot.user;
731
820
  return {
732
821
  align: snapshot.status === "ready" && isAlignPreference(snapshot.value?.align) ? snapshot.value.align : DEFAULT_ALIGN,
733
- overridden: user !== void 0 && user.align !== void 0
822
+ pageSize: snapshot.status === "ready" && isPageSize(snapshot.value?.pageSize) ? snapshot.value.pageSize : 5,
823
+ overridden: user !== void 0 && (user.align !== void 0 || user.pageSize !== void 0)
734
824
  };
735
825
  }
736
826
  /** Release the scope subscription (bound on the settings fiber's lifecycle). */
@@ -754,6 +844,11 @@ window.__ModuleLoader__.load({
754
844
  if (this.scope === void 0) return;
755
845
  this.scope.set(ALIGN_FIELD, align);
756
846
  }
847
+ /** Route the user's page-size choice to the Host document. */
848
+ setPageSize(pageSize) {
849
+ if (this.scope === void 0) return;
850
+ this.scope.set(PAGE_SIZE_FIELD, pageSize);
851
+ }
757
852
  };
758
853
  //#endregion
759
854
  //#region src/client/locales.ts
@@ -763,8 +858,8 @@ window.__ModuleLoader__.load({
763
858
  */
764
859
  const zh = {
765
860
  "strip.empty": "本会话还没有提问",
766
- "strip.up": "向上翻出 5 个提问圆点",
767
- "strip.down": "向下翻出 5 个提问圆点",
861
+ "strip.up": "向上翻出更多提问圆点",
862
+ "strip.down": "向下翻出更多提问圆点",
768
863
  "jump.inactive": "聊天视图未激活",
769
864
  "jump.hidden": "目标无独立气泡,已定位到邻近内容",
770
865
  "jump.notfound": "目标未加载或不存在(可能已压缩)",
@@ -773,12 +868,14 @@ window.__ModuleLoader__.load({
773
868
  "settings.align.title": "导航条对齐",
774
869
  "settings.align.desc": "选择圆点导航条锚定在对话栏的哪一侧。",
775
870
  "settings.align.left": "左侧",
776
- "settings.align.right": "右侧"
871
+ "settings.align.right": "右侧",
872
+ "settings.pagesize.title": "每次翻出数量",
873
+ "settings.pagesize.desc": "点击 ▲/▼ 三角按钮时,每次翻出的隐藏圆点数量。"
777
874
  };
778
875
  const en = {
779
876
  "strip.empty": "No questions in this session yet",
780
- "strip.up": "Reveal 5 question dots above",
781
- "strip.down": "Reveal 5 question dots below",
877
+ "strip.up": "Reveal more question dots above",
878
+ "strip.down": "Reveal more question dots below",
782
879
  "jump.inactive": "Chat view is not active",
783
880
  "jump.hidden": "No dedicated bubble; landed on nearby content",
784
881
  "jump.notfound": "Target not loaded or missing (maybe compacted)",
@@ -787,7 +884,9 @@ window.__ModuleLoader__.load({
787
884
  "settings.align.title": "Rail alignment",
788
885
  "settings.align.desc": "Choose which edge of the conversation column the dot rail anchors to.",
789
886
  "settings.align.left": "Left",
790
- "settings.align.right": "Right"
887
+ "settings.align.right": "Right",
888
+ "settings.pagesize.title": "Dots per page",
889
+ "settings.pagesize.desc": "How many hidden dots one click of the ▲/▼ paging buttons reveals."
791
890
  };
792
891
  //#endregion
793
892
  //#region src/core/nodes.ts
@@ -1017,8 +1116,10 @@ window.__ModuleLoader__.load({
1017
1116
  jumpToQuestion(ports, key);
1018
1117
  },
1019
1118
  align: () => settings.getSnapshot().align,
1020
- subscribeAlign: (cb) => settings.subscribe(cb),
1021
- setAlign: (align) => settings.setAlign(align)
1119
+ subscribeSettings: (cb) => settings.subscribe(cb),
1120
+ setAlign: (align) => settings.setAlign(align),
1121
+ pageSize: () => settings.getSnapshot().pageSize,
1122
+ setPageSize: (pageSize) => settings.setPageSize(pageSize)
1022
1123
  };
1023
1124
  }
1024
1125
  /**