@luziyang2026/dsh-question-nav 0.7.0 → 0.7.2

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
@@ -40,7 +40,8 @@ Package: **`@luziyang2026/dsh-question-nav`** ([npm][npm] · [GitHub][github]).
40
40
  never touched), the official projection cache persists it across restarts,
41
41
  and push frames deliver new questions live.
42
42
  - **Hover (focus)**: the dot and its **two neighbors on each side** magnify
43
- progressively (selected largest), the rail auto-centers the selection, and a
43
+ progressively (selected largest), the rail scrolls the selection into view
44
+ only when clipped by the band's edges, and a
44
45
  **vertical cascade of five crisp question cards** (portal-rendered, no
45
46
  native-title delay) appears beside the rail — one card per window dot,
46
47
  stacked top-to-bottom without overlap and centered on the selected dot. Each
@@ -55,10 +56,11 @@ Package: **`@luziyang2026/dsh-question-nav`** ([npm][npm] · [GitHub][github]).
55
56
  - **Click**: jumps to that question. Only then does the jump loop page the
56
57
  window (`loadOlder()`) to bring that specific page into view — never the
57
58
  whole history up front.
58
- - **Elegant overflow scroll**: when many dots don't fit the 60% band, the
59
- native scrollbar is hidden and the column fades out at its top and bottom
60
- edges; hovering either fade zone gently auto-scrolls so hidden dots flow
61
- into the clear, clickable area (wheel and trackpad still scroll too).
59
+ - **Paged overflow**: when many dots don't fit the 60% band, the native
60
+ 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 auto-scrolls.
62
64
  - Empty/left areas of the rail pass pointer events through to the conversation
63
65
  (it never blocks the chat).
64
66
 
package/README.zh.md CHANGED
@@ -33,7 +33,8 @@
33
33
  分页窗口完全不被动),官方 projection-cache 跨重启持久化,新提问通过
34
34
  推送帧实时到达。
35
35
  - **悬停(聚焦)**:圆点与其**上下各两个**圆点一起渐进放大(选中最大),
36
- 导航条自动把选中点居中;导航条旁出现一列**竖排的 5 张清晰问题卡片**
36
+ 选中点仅在被带边缘遮挡时才最小滚动到可视区(不强制居中,逐点浏览时
37
+ 点列保持不动);导航条旁出现一列**竖排的 5 张清晰问题卡片**
37
38
  (portal 渲染,无原生 `title` 延迟)——窗口内每个圆点一张卡片,上下排布、
38
39
  **互不遮挡**,以选中点为中心垂直居中。每张卡片显示该轮提问**全文**(一轮
39
40
  多条提问时全部列出)与提问的**发出时间**(当天 `HH:MM`,跨天
@@ -44,9 +45,10 @@
44
45
  效果一致。
45
46
  - **点击**:跳转到该提问;只有这时跳转循环才 `loadOlder()` 扩窗,把**那一页**
46
47
  带进视图——绝不会一次性展开整个历史。
47
- - **优雅的溢出滚动**:圆点太多超出 60% 限高时,原生滚动条被隐藏,点列在
48
- 上下边缘渐隐;鼠标悬停任一渐隐区会轻柔自动滚动,把藏起来的圆点流入
49
- 清晰可点的区域(滚轮/触控板同样可滚)。
48
+ - **分页式溢出**:圆点太多超出 60% 限高时,原生滚动条被隐藏,点列上下会出现
49
+ 两个与圆点同风格的三角按钮——**队列上方 ▲、队列下方 ▼**——每次点击翻出
50
+ 5 个隐藏圆点。点列上下边缘的渐隐只是纯视觉提示(滚轮/触控板同样可滚);
51
+ 悬停不再自动滚动。
50
52
  - 圆点列空白区域**点穿**到对话内容,不会挡住聊天。
51
53
 
52
54
  ## 环境要求
package/lib/client.js CHANGED
@@ -141,13 +141,28 @@ window.__ModuleLoader__.load({
141
141
  }
142
142
  }
143
143
  /**
144
- * Auto-scroll speed (px/s) for a pointer at `ratio` depth into an edge fade
145
- * zone (0 = at the zone boundary, 1 = at the band's very edge): eases
146
- * linearly from MIN to MAX so a shallow probe scrolls gently and pushing
147
- * into the edge moves fast. The sign (direction) is applied by the caller.
144
+ * Minimal "scroll into view" for the focused dot, in the spirit of
145
+ * scrollIntoView({ block: 'nearest' }): returns the scrollTop that brings the
146
+ * dot plus `clearance` room for its magnified neighbors inside the
147
+ * visible band with the smallest possible movement, or null when the dot is
148
+ * already fully visible. Unlike unconditional centering this never shifts the
149
+ * band while the user browses dot-by-dot: only a clipped dot is scrolled.
148
150
  */
149
- function edgeScrollSpeed(ratio) {
150
- return 90 + 330 * Math.max(0, Math.min(1, ratio));
151
+ function minimalScrollIntoView(scrollTop, clientHeight, scrollHeight, dotTop, dotHeight, clearance = 50) {
152
+ const margin = Math.min(clearance, clientHeight / 4);
153
+ const viewTop = scrollTop + margin;
154
+ const viewBottom = scrollTop + clientHeight - margin;
155
+ if (dotTop >= viewTop && dotTop + dotHeight <= viewBottom) return null;
156
+ const next = dotTop < viewTop ? dotTop - margin : dotTop + dotHeight + margin - clientHeight;
157
+ return Math.max(0, Math.min(next, Math.max(0, scrollHeight - clientHeight)));
158
+ }
159
+ /** Scroll step (px) for one paging click: `rows` whole dot rows. */
160
+ function pageStep(dotSize = 8, gap = 6, rows = 5) {
161
+ return rows * (dotSize + gap);
162
+ }
163
+ /** Clamp a target scrollTop into the band's valid range (0..maxScroll). */
164
+ function clampScrollTop(target, maxScroll) {
165
+ return Math.max(0, Math.min(target, Math.max(0, maxScroll)));
151
166
  }
152
167
  //#endregion
153
168
  //#region src/core/time.ts
@@ -187,7 +202,7 @@ window.__ModuleLoader__.load({
187
202
  }
188
203
  //#endregion
189
204
  //#region \0dsh-css:dsh-question-nav/src/client/question-nav.module.css.mjs
190
- 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_list{scrollbar-width:none;pointer-events:auto;flex-direction:column;flex:0 auto;align-items:center;gap:6px;width:100%;min-height:0;max-height:60%;margin:auto 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_list>:first-child{margin-top:auto}.TWf_pa_list>:last-child{margin-bottom:auto}.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_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)}";
205
+ 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)}";
191
206
  const tagId = "@luziyang2026/dsh-question-nav/question-nav.module.css";
192
207
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
193
208
  const tag = document.createElement("style");
@@ -214,6 +229,10 @@ window.__ModuleLoader__.load({
214
229
  "hint": "TWf_pa_hint",
215
230
  "list": "TWf_pa_list",
216
231
  "listScrollable": "TWf_pa_listScrollable",
232
+ "navBtn": "TWf_pa_navBtn",
233
+ "navDown": "TWf_pa_navDown",
234
+ "navUp": "TWf_pa_navUp",
235
+ "queue": "TWf_pa_queue",
217
236
  "rail": "TWf_pa_rail",
218
237
  "railRight": "TWf_pa_railRight",
219
238
  "segment": "TWf_pa_segment",
@@ -239,8 +258,13 @@ window.__ModuleLoader__.load({
239
258
  * vertical cascade of question cards opens — the selected (center) card is the
240
259
  * focus (brand accent, elevated, full question text), the four neighbors are
241
260
  * narrower context cards clamped to fewer lines. Every card is clickable and
242
- * jumps to its question, exactly like clicking the dot; the rail auto-centers
243
- * the selected dot.
261
+ * jumps to its question, exactly like clicking the dot; the rail scrolls the
262
+ * selected dot into view only when it is clipped by the band's edges.
263
+ *
264
+ * Overflow is paged, not auto-scrolled: two small triangle buttons in the dot
265
+ * style sit above and below the dot queue (▲ / ▼), each click revealing five
266
+ * hidden dots. The native scrollbar stays hidden and the column fades at its
267
+ * edges as a pure visual cue — no hover auto-scroll.
244
268
  *
245
269
  * Data source: the host-folded `questionIndex` session projection (whole
246
270
  * history, persisted host-side, pushed live through session/projection
@@ -297,51 +321,28 @@ window.__ModuleLoader__.load({
297
321
  const lastDotsRef = (0, react.useRef)([]);
298
322
  const lastFocusedKeyRef = (0, react.useRef)(null);
299
323
  const [scrollable, setScrollable] = (0, react.useState)(false);
300
- const edgeScrollRef = (0, react.useRef)({
301
- speed: 0,
302
- raf: 0,
303
- last: 0
324
+ const [scrollPos, setScrollPos] = (0, react.useState)({
325
+ top: 0,
326
+ max: 0
304
327
  });
305
- const stopEdgeScroll = () => {
306
- edgeScrollRef.current.speed = 0;
307
- if (edgeScrollRef.current.raf !== 0) {
308
- cancelAnimationFrame(edgeScrollRef.current.raf);
309
- edgeScrollRef.current.raf = 0;
310
- }
311
- };
312
- const edgeTick = (now) => {
328
+ const syncScroll = () => {
313
329
  const list = listRef.current;
314
- const state = edgeScrollRef.current;
315
- state.raf = 0;
316
- if (list === null || state.speed === 0) return;
317
- const dt = Math.min(64, now - state.last) / 1e3;
318
- state.last = now;
319
- list.scrollTop = list.scrollTop + state.speed * dt;
320
- const atTop = list.scrollTop <= 0 && state.speed < 0;
321
- const atBottom = list.scrollTop >= list.scrollHeight - list.clientHeight - 1 && state.speed > 0;
322
- if (atTop || atBottom) {
323
- state.speed = 0;
324
- return;
325
- }
326
- state.raf = requestAnimationFrame(edgeTick);
330
+ if (list === null) return;
331
+ const max = Math.max(0, list.scrollHeight - list.clientHeight);
332
+ setScrollPos({
333
+ top: list.scrollTop,
334
+ max
335
+ });
327
336
  };
328
- const onListMouseMove = (e) => {
337
+ const pageBy = (dir) => {
329
338
  const list = listRef.current;
330
339
  if (list === null) return;
331
- const rect = list.getBoundingClientRect();
332
- const y = e.clientY - rect.top;
333
- const depthTop = 26 - y;
334
- const depthBottom = y - (rect.height - 26);
335
- let speed = 0;
336
- if (depthTop > 0 && list.scrollTop > 0) speed = -edgeScrollSpeed(depthTop / 26);
337
- else if (depthBottom > 0 && list.scrollTop < list.scrollHeight - list.clientHeight - 1) speed = edgeScrollSpeed(depthBottom / 26);
338
- const state = edgeScrollRef.current;
339
- state.speed = speed;
340
- if (speed !== 0 && state.raf === 0) {
341
- state.last = performance.now();
342
- state.raf = requestAnimationFrame(edgeTick);
343
- }
340
+ const max = Math.max(0, list.scrollHeight - list.clientHeight);
341
+ list.scrollTop = clampScrollTop(list.scrollTop + dir * pageStep(), max);
342
+ syncScroll();
344
343
  };
344
+ const canPageUp = scrollable && scrollPos.top > 0;
345
+ const canPageDown = scrollable && scrollPos.top < scrollPos.max;
345
346
  const cancelClearFocus = () => {
346
347
  if (clearFocusTimerRef.current !== null) {
347
348
  window.clearTimeout(clearFocusTimerRef.current);
@@ -485,13 +486,17 @@ window.__ModuleLoader__.load({
485
486
  if (list === null) return;
486
487
  const target = list.querySelector("[data-question-nav-focused=\"true\"]");
487
488
  if (target === null) return;
488
- const center = target.offsetTop - list.offsetTop - (list.clientHeight - target.offsetHeight) / 2;
489
- list.scrollTop = Math.max(0, Math.min(center, list.scrollHeight - list.clientHeight));
489
+ const next = minimalScrollIntoView(list.scrollTop, list.clientHeight, list.scrollHeight, target.offsetTop - list.offsetTop, target.offsetHeight);
490
+ if (next !== null) list.scrollTop = next;
491
+ syncScroll();
490
492
  }, [focus]);
491
493
  (0, react.useLayoutEffect)(() => {
492
494
  const list = listRef.current;
493
495
  if (list === null) return;
494
- const check = () => setScrollable(list.scrollHeight > list.clientHeight + 1);
496
+ const check = () => {
497
+ setScrollable(list.scrollHeight > list.clientHeight + 1);
498
+ syncScroll();
499
+ };
495
500
  check();
496
501
  if (typeof ResizeObserver === "undefined") return;
497
502
  const observer = new ResizeObserver(check);
@@ -501,7 +506,6 @@ window.__ModuleLoader__.load({
501
506
  (0, react.useEffect)(() => () => {
502
507
  if (hintTimerRef.current !== null) window.clearTimeout(hintTimerRef.current);
503
508
  if (clearFocusTimerRef.current !== null) window.clearTimeout(clearFocusTimerRef.current);
504
- stopEdgeScroll();
505
509
  }, []);
506
510
  if (!visible) return null;
507
511
  const onJump = (dot) => {
@@ -538,39 +542,60 @@ window.__ModuleLoader__.load({
538
542
  "data-question-nav": "rail",
539
543
  children: [
540
544
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
541
- ref: listRef,
542
- className: scrollable ? `${question_nav_module_css_default.list} ${question_nav_module_css_default.listScrollable}` : question_nav_module_css_default.list,
543
- onMouseMove: scrollable ? onListMouseMove : void 0,
544
- onMouseLeave: () => {
545
- stopEdgeScroll();
546
- scheduleClearFocus();
547
- },
545
+ className: question_nav_module_css_default.queue,
548
546
  children: dots.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
549
547
  className: question_nav_module_css_default.empty,
550
548
  children: t("strip.empty")
551
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
552
- className: question_nav_module_css_default.dots,
553
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
549
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
550
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
554
551
  className: question_nav_module_css_default.count,
555
552
  children: dots.length
556
- }), dots.map((dot, index) => {
557
- const isFocused = focus !== null && focus.key === dot.key;
558
- const tier = selectedIndex < 0 ? null : focusTier(index - selectedIndex);
559
- const scale = jumpingKey === dot.key ? 1.6 : tier !== null ? focusScale(tier) : 1;
560
- const cls = [question_nav_module_css_default.dot];
561
- if (isFocused) cls.push(question_nav_module_css_default.focused);
562
- if (jumpingKey === dot.key) cls.push(question_nav_module_css_default.active);
563
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
564
- className: cls.join(" "),
565
- style: { transform: `scale(${scale})` },
566
- "data-question-nav-focused": isFocused ? "true" : void 0,
567
- "data-question-nav-index": index,
568
- "aria-label": dot.texts[0] ?? "",
569
- onMouseEnter: (e) => openFocus(dot, e.currentTarget),
570
- onClick: () => onJump(dot)
571
- }, dot.key);
572
- })]
573
- })
553
+ }),
554
+ scrollable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
555
+ type: "button",
556
+ className: `${question_nav_module_css_default.navBtn} ${question_nav_module_css_default.navUp}`,
557
+ "aria-label": t("strip.up"),
558
+ style: { visibility: canPageUp ? "visible" : "hidden" },
559
+ onMouseEnter: cancelClearFocus,
560
+ onMouseLeave: scheduleClearFocus,
561
+ onClick: () => pageBy(-1)
562
+ }) : null,
563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
564
+ ref: listRef,
565
+ className: scrollable ? `${question_nav_module_css_default.list} ${question_nav_module_css_default.listScrollable}` : question_nav_module_css_default.list,
566
+ onScroll: syncScroll,
567
+ onMouseLeave: scheduleClearFocus,
568
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
569
+ className: question_nav_module_css_default.dots,
570
+ children: dots.map((dot, index) => {
571
+ const isFocused = focus !== null && focus.key === dot.key;
572
+ const tier = selectedIndex < 0 ? null : focusTier(index - selectedIndex);
573
+ const scale = jumpingKey === dot.key ? 1.6 : tier !== null ? focusScale(tier) : 1;
574
+ const cls = [question_nav_module_css_default.dot];
575
+ if (isFocused) cls.push(question_nav_module_css_default.focused);
576
+ if (jumpingKey === dot.key) cls.push(question_nav_module_css_default.active);
577
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
578
+ className: cls.join(" "),
579
+ style: { transform: `scale(${scale})` },
580
+ "data-question-nav-focused": isFocused ? "true" : void 0,
581
+ "data-question-nav-index": index,
582
+ "aria-label": dot.texts[0] ?? "",
583
+ onMouseEnter: (e) => openFocus(dot, e.currentTarget),
584
+ onClick: () => onJump(dot)
585
+ }, dot.key);
586
+ })
587
+ })
588
+ }),
589
+ scrollable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
590
+ type: "button",
591
+ className: `${question_nav_module_css_default.navBtn} ${question_nav_module_css_default.navDown}`,
592
+ "aria-label": t("strip.down"),
593
+ style: { visibility: canPageDown ? "visible" : "hidden" },
594
+ onMouseEnter: cancelClearFocus,
595
+ onMouseLeave: scheduleClearFocus,
596
+ onClick: () => pageBy(1)
597
+ }) : null
598
+ ] })
574
599
  }),
575
600
  hint !== null ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
576
601
  className: question_nav_module_css_default.hint,
@@ -768,6 +793,8 @@ window.__ModuleLoader__.load({
768
793
  */
769
794
  const zh = {
770
795
  "strip.empty": "本会话还没有提问",
796
+ "strip.up": "向上翻出 5 个提问圆点",
797
+ "strip.down": "向下翻出 5 个提问圆点",
771
798
  "jump.inactive": "聊天视图未激活",
772
799
  "jump.hidden": "目标无独立气泡,已定位到邻近内容",
773
800
  "jump.notfound": "目标未加载或不存在(可能已压缩)",
@@ -780,6 +807,8 @@ window.__ModuleLoader__.load({
780
807
  };
781
808
  const en = {
782
809
  "strip.empty": "No questions in this session yet",
810
+ "strip.up": "Reveal 5 question dots above",
811
+ "strip.down": "Reveal 5 question dots below",
783
812
  "jump.inactive": "Chat view is not active",
784
813
  "jump.hidden": "No dedicated bubble; landed on nearby content",
785
814
  "jump.notfound": "Target not loaded or missing (maybe compacted)",