@luziyang2026/dsh-question-nav 0.7.2 → 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 +12 -4
- package/README.zh.md +10 -3
- package/lib/client.js +128 -57
- package/lib/client.js.map +1 -1
- package/lib/index.js +30 -5
- package/lib/types/client/QuestionNavSettingsTab.d.ts +4 -3
- package/lib/types/client/QuestionNavStrip.d.ts +7 -2
- package/lib/types/client/locales.d.ts +8 -4
- package/lib/types/client/settings.d.ts +13 -7
- package/lib/types/core/focus.d.ts +3 -16
- package/lib/types/core/page-size.d.ts +16 -0
- package/lib/types/settings.d.ts +8 -4
- package/package.json +1 -1
- package/src/client/QuestionNavSettingsTab.tsx +24 -5
- package/src/client/QuestionNavStrip.tsx +82 -47
- package/src/client/index.ts +3 -1
- package/src/client/locales.ts +8 -4
- package/src/client/question-nav.module.css +61 -17
- package/src/client/settings.ts +36 -13
- package/src/core/focus.ts +3 -34
- package/src/core/page-size.ts +20 -0
- package/src/settings.ts +12 -4
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
|
-

|
|
25
|
+
|
|
26
|
+
Plugin settings (rail alignment + dots per page):
|
|
27
|
+
|
|
28
|
+

|
|
25
29
|
|
|
26
30
|
## What it does
|
|
27
31
|
|
|
@@ -58,9 +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
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
64
72
|
- Empty/left areas of the rail pass pointer events through to the conversation
|
|
65
73
|
(it never blocks the chat).
|
|
66
74
|
|
package/README.zh.md
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
## 效果展示
|
|
20
20
|
|
|
21
|
-

|
|
22
|
+
|
|
23
|
+
插件设置页(导航条对齐 + 每次翻出数量):
|
|
24
|
+
|
|
25
|
+

|
|
22
26
|
|
|
23
27
|
## 功能
|
|
24
28
|
|
|
@@ -47,8 +51,11 @@
|
|
|
47
51
|
带进视图——绝不会一次性展开整个历史。
|
|
48
52
|
- **分页式溢出**:圆点太多超出 60% 限高时,原生滚动条被隐藏,点列上下会出现
|
|
49
53
|
两个与圆点同风格的三角按钮——**队列上方 ▲、队列下方 ▼**——每次点击翻出
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
下一页隐藏圆点,新翻出的圆点会带一个简短的逐个点弹入动画作为点击反馈。
|
|
55
|
+
每页数量可在 **设置 → 插件 → 提问导航 → 每次翻出数量** 中配置
|
|
56
|
+
(3 / 5 / 8 / 10,默认 5)。三角按钮本身就是溢出提示——只有对应方向还有
|
|
57
|
+
更多圆点时它才会出现(滚轮/触控板同样可滚);**悬停绝不滚动点列**,
|
|
58
|
+
浏览时点列保持不动。
|
|
52
59
|
- 圆点列空白区域**点穿**到对话内容,不会挡住聊天。
|
|
53
60
|
|
|
54
61
|
## 环境要求
|
package/lib/client.js
CHANGED
|
@@ -140,22 +140,6 @@ window.__ModuleLoader__.load({
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
/**
|
|
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.
|
|
150
|
-
*/
|
|
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
143
|
/** Scroll step (px) for one paging click: `rows` whole dot rows. */
|
|
160
144
|
function pageStep(dotSize = 8, gap = 6, rows = 5) {
|
|
161
145
|
return rows * (dotSize + gap);
|
|
@@ -202,7 +186,7 @@ window.__ModuleLoader__.load({
|
|
|
202
186
|
}
|
|
203
187
|
//#endregion
|
|
204
188
|
//#region \0dsh-css:dsh-question-nav/src/client/question-nav.module.css.mjs
|
|
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}.
|
|
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)}";
|
|
206
190
|
const tagId = "@luziyang2026/dsh-question-nav/question-nav.module.css";
|
|
207
191
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
208
192
|
const tag = document.createElement("style");
|
|
@@ -223,12 +207,13 @@ window.__ModuleLoader__.load({
|
|
|
223
207
|
"cascade": "TWf_pa_cascade",
|
|
224
208
|
"count": "TWf_pa_count",
|
|
225
209
|
"dot": "TWf_pa_dot",
|
|
210
|
+
"dotEnterFromBottom": "TWf_pa_dotEnterFromBottom",
|
|
211
|
+
"dotEnterFromTop": "TWf_pa_dotEnterFromTop",
|
|
226
212
|
"dots": "TWf_pa_dots",
|
|
227
213
|
"empty": "TWf_pa_empty",
|
|
228
214
|
"focused": "TWf_pa_focused",
|
|
229
215
|
"hint": "TWf_pa_hint",
|
|
230
216
|
"list": "TWf_pa_list",
|
|
231
|
-
"listScrollable": "TWf_pa_listScrollable",
|
|
232
217
|
"navBtn": "TWf_pa_navBtn",
|
|
233
218
|
"navDown": "TWf_pa_navDown",
|
|
234
219
|
"navUp": "TWf_pa_navUp",
|
|
@@ -258,13 +243,16 @@ window.__ModuleLoader__.load({
|
|
|
258
243
|
* vertical cascade of question cards opens — the selected (center) card is the
|
|
259
244
|
* focus (brand accent, elevated, full question text), the four neighbors are
|
|
260
245
|
* narrower context cards clamped to fewer lines. Every card is clickable and
|
|
261
|
-
* jumps to its question, exactly like clicking the dot
|
|
262
|
-
*
|
|
246
|
+
* jumps to its question, exactly like clicking the dot. Hovering never scrolls
|
|
247
|
+
* the band — the dot column stays put while you browse.
|
|
263
248
|
*
|
|
264
249
|
* 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
|
|
266
|
-
* hidden dots
|
|
267
|
-
*
|
|
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.
|
|
268
256
|
*
|
|
269
257
|
* Data source: the host-folded `questionIndex` session projection (whole
|
|
270
258
|
* history, persisted host-side, pushed live through session/projection
|
|
@@ -314,17 +302,19 @@ window.__ModuleLoader__.load({
|
|
|
314
302
|
const [hint, setHint] = (0, react.useState)(null);
|
|
315
303
|
const [focus, setFocus] = (0, react.useState)(null);
|
|
316
304
|
const [align, setAlign] = (0, react.useState)(() => props.align());
|
|
305
|
+
const [pageSize, setPageSize] = (0, react.useState)(() => props.pageSize());
|
|
317
306
|
const panelRef = (0, react.useRef)(null);
|
|
318
307
|
const listRef = (0, react.useRef)(null);
|
|
319
308
|
const hintTimerRef = (0, react.useRef)(null);
|
|
320
309
|
const clearFocusTimerRef = (0, react.useRef)(null);
|
|
321
310
|
const lastDotsRef = (0, react.useRef)([]);
|
|
322
|
-
const lastFocusedKeyRef = (0, react.useRef)(null);
|
|
323
311
|
const [scrollable, setScrollable] = (0, react.useState)(false);
|
|
324
312
|
const [scrollPos, setScrollPos] = (0, react.useState)({
|
|
325
313
|
top: 0,
|
|
326
314
|
max: 0
|
|
327
315
|
});
|
|
316
|
+
const [revealed, setRevealed] = (0, react.useState)(null);
|
|
317
|
+
const revealTimerRef = (0, react.useRef)(null);
|
|
328
318
|
const syncScroll = () => {
|
|
329
319
|
const list = listRef.current;
|
|
330
320
|
if (list === null) return;
|
|
@@ -338,8 +328,33 @@ window.__ModuleLoader__.load({
|
|
|
338
328
|
const list = listRef.current;
|
|
339
329
|
if (list === null) return;
|
|
340
330
|
const max = Math.max(0, list.scrollHeight - list.clientHeight);
|
|
341
|
-
|
|
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;
|
|
342
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);
|
|
343
358
|
};
|
|
344
359
|
const canPageUp = scrollable && scrollPos.top > 0;
|
|
345
360
|
const canPageDown = scrollable && scrollPos.top < scrollPos.max;
|
|
@@ -353,7 +368,10 @@ window.__ModuleLoader__.load({
|
|
|
353
368
|
cancelClearFocus();
|
|
354
369
|
clearFocusTimerRef.current = window.setTimeout(() => setFocus(null), 240);
|
|
355
370
|
};
|
|
356
|
-
(0, react.useEffect)(() => props.
|
|
371
|
+
(0, react.useEffect)(() => props.subscribeSettings(() => {
|
|
372
|
+
setAlign(props.align());
|
|
373
|
+
setPageSize(props.pageSize());
|
|
374
|
+
}), [props]);
|
|
357
375
|
const showHint = (message) => {
|
|
358
376
|
setHint(message);
|
|
359
377
|
if (hintTimerRef.current !== null) window.clearTimeout(hintTimerRef.current);
|
|
@@ -477,19 +495,6 @@ window.__ModuleLoader__.load({
|
|
|
477
495
|
window.removeEventListener("resize", wake);
|
|
478
496
|
};
|
|
479
497
|
}, [visible, align]);
|
|
480
|
-
(0, react.useLayoutEffect)(() => {
|
|
481
|
-
const key = focus?.key ?? null;
|
|
482
|
-
if (lastFocusedKeyRef.current === key) return;
|
|
483
|
-
lastFocusedKeyRef.current = key;
|
|
484
|
-
if (key === null) return;
|
|
485
|
-
const list = listRef.current;
|
|
486
|
-
if (list === null) return;
|
|
487
|
-
const target = list.querySelector("[data-question-nav-focused=\"true\"]");
|
|
488
|
-
if (target === null) return;
|
|
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();
|
|
492
|
-
}, [focus]);
|
|
493
498
|
(0, react.useLayoutEffect)(() => {
|
|
494
499
|
const list = listRef.current;
|
|
495
500
|
if (list === null) return;
|
|
@@ -506,6 +511,7 @@ window.__ModuleLoader__.load({
|
|
|
506
511
|
(0, react.useEffect)(() => () => {
|
|
507
512
|
if (hintTimerRef.current !== null) window.clearTimeout(hintTimerRef.current);
|
|
508
513
|
if (clearFocusTimerRef.current !== null) window.clearTimeout(clearFocusTimerRef.current);
|
|
514
|
+
if (revealTimerRef.current !== null) window.clearTimeout(revealTimerRef.current);
|
|
509
515
|
}, []);
|
|
510
516
|
if (!visible) return null;
|
|
511
517
|
const onJump = (dot) => {
|
|
@@ -562,7 +568,7 @@ window.__ModuleLoader__.load({
|
|
|
562
568
|
}) : null,
|
|
563
569
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
564
570
|
ref: listRef,
|
|
565
|
-
className:
|
|
571
|
+
className: question_nav_module_css_default.list,
|
|
566
572
|
onScroll: syncScroll,
|
|
567
573
|
onMouseLeave: scheduleClearFocus,
|
|
568
574
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -571,14 +577,20 @@ window.__ModuleLoader__.load({
|
|
|
571
577
|
const isFocused = focus !== null && focus.key === dot.key;
|
|
572
578
|
const tier = selectedIndex < 0 ? null : focusTier(index - selectedIndex);
|
|
573
579
|
const scale = jumpingKey === dot.key ? 1.6 : tier !== null ? focusScale(tier) : 1;
|
|
580
|
+
const reveal = revealed?.get(dot.key);
|
|
574
581
|
const cls = [question_nav_module_css_default.dot];
|
|
575
582
|
if (isFocused) cls.push(question_nav_module_css_default.focused);
|
|
576
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);
|
|
577
585
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
578
586
|
className: cls.join(" "),
|
|
579
|
-
style: {
|
|
587
|
+
style: {
|
|
588
|
+
transform: `scale(${scale})`,
|
|
589
|
+
...reveal !== void 0 ? { animationDelay: `${reveal.order * 35}ms` } : {}
|
|
590
|
+
},
|
|
580
591
|
"data-question-nav-focused": isFocused ? "true" : void 0,
|
|
581
592
|
"data-question-nav-index": index,
|
|
593
|
+
"data-question-nav-key": dot.key,
|
|
582
594
|
"aria-label": dot.texts[0] ?? "",
|
|
583
595
|
onMouseEnter: (e) => openFocus(dot, e.currentTarget),
|
|
584
596
|
onClick: () => onJump(dot)
|
|
@@ -676,24 +688,45 @@ window.__ModuleLoader__.load({
|
|
|
676
688
|
/** Field carrying the selected anchor edge. */
|
|
677
689
|
const ALIGN_FIELD = "align";
|
|
678
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
|
|
679
710
|
//#region src/client/QuestionNavSettingsTab.tsx
|
|
680
711
|
/**
|
|
681
712
|
* The plugin's settings page inside the shell's Plugins section
|
|
682
|
-
* (`settings.plugins.tab`):
|
|
683
|
-
* conversation column the dot rail anchors to
|
|
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
|
|
684
716
|
* `question-nav` settings namespace (registered by the host half); the strip
|
|
685
|
-
* re-anchors live when the snapshot changes.
|
|
717
|
+
* re-anchors and re-steps live when the snapshot changes.
|
|
686
718
|
*
|
|
687
719
|
* @module dsh-question-nav/client/settings-tab
|
|
688
720
|
*/
|
|
689
721
|
/** Re-render on settings snapshot changes (the register inject face is static). */
|
|
690
|
-
function
|
|
722
|
+
function useSettingsTick(subscribe) {
|
|
691
723
|
const [, bump] = (0, react.useState)(0);
|
|
692
724
|
(0, react.useEffect)(() => subscribe(() => bump((n) => n + 1)), [subscribe]);
|
|
693
725
|
}
|
|
694
726
|
function QuestionNavSettingsTab(props) {
|
|
695
|
-
|
|
727
|
+
useSettingsTick(props.subscribeSettings);
|
|
696
728
|
const align = props.align();
|
|
729
|
+
const pageSize = props.pageSize();
|
|
697
730
|
const t = props.t;
|
|
698
731
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
699
732
|
className: question_nav_module_css_default.settings,
|
|
@@ -718,16 +751,41 @@ window.__ModuleLoader__.load({
|
|
|
718
751
|
onClick: () => props.setAlign(option),
|
|
719
752
|
children: t(option === "left" ? "settings.align.left" : "settings.align.right")
|
|
720
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))
|
|
721
775
|
})
|
|
722
776
|
]
|
|
723
777
|
});
|
|
724
778
|
}
|
|
725
779
|
//#endregion
|
|
726
780
|
//#region src/client/settings.ts
|
|
727
|
-
/** Narrow a raw section to the anchor field. */
|
|
781
|
+
/** Narrow a raw section value to the anchor field. */
|
|
728
782
|
function isAlignPreference(value) {
|
|
729
783
|
return ALIGN_OPTIONS.some((option) => option === value);
|
|
730
784
|
}
|
|
785
|
+
/** Narrow a raw section value to the page-size field. */
|
|
786
|
+
function isPageSize(value) {
|
|
787
|
+
return PAGE_SIZE_OPTIONS.includes(value);
|
|
788
|
+
}
|
|
731
789
|
/** Reactive handle over the plugin's durable settings section. */
|
|
732
790
|
var QuestionNavSettingsController = class {
|
|
733
791
|
scope;
|
|
@@ -735,6 +793,7 @@ window.__ModuleLoader__.load({
|
|
|
735
793
|
unsubscribe = () => {};
|
|
736
794
|
state = {
|
|
737
795
|
align: DEFAULT_ALIGN,
|
|
796
|
+
pageSize: 5,
|
|
738
797
|
overridden: false
|
|
739
798
|
};
|
|
740
799
|
/**
|
|
@@ -751,7 +810,7 @@ window.__ModuleLoader__.load({
|
|
|
751
810
|
this.unsubscribe = this.scope.subscribe(() => {
|
|
752
811
|
if (this.scope === void 0) return;
|
|
753
812
|
const next = this.derive(this.scope.getSnapshot());
|
|
754
|
-
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;
|
|
755
814
|
this.state = next;
|
|
756
815
|
for (const listener of this.listeners) listener();
|
|
757
816
|
});
|
|
@@ -760,7 +819,8 @@ window.__ModuleLoader__.load({
|
|
|
760
819
|
const user = snapshot.user;
|
|
761
820
|
return {
|
|
762
821
|
align: snapshot.status === "ready" && isAlignPreference(snapshot.value?.align) ? snapshot.value.align : DEFAULT_ALIGN,
|
|
763
|
-
|
|
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)
|
|
764
824
|
};
|
|
765
825
|
}
|
|
766
826
|
/** Release the scope subscription (bound on the settings fiber's lifecycle). */
|
|
@@ -784,6 +844,11 @@ window.__ModuleLoader__.load({
|
|
|
784
844
|
if (this.scope === void 0) return;
|
|
785
845
|
this.scope.set(ALIGN_FIELD, align);
|
|
786
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
|
+
}
|
|
787
852
|
};
|
|
788
853
|
//#endregion
|
|
789
854
|
//#region src/client/locales.ts
|
|
@@ -793,8 +858,8 @@ window.__ModuleLoader__.load({
|
|
|
793
858
|
*/
|
|
794
859
|
const zh = {
|
|
795
860
|
"strip.empty": "本会话还没有提问",
|
|
796
|
-
"strip.up": "
|
|
797
|
-
"strip.down": "
|
|
861
|
+
"strip.up": "向上翻出更多提问圆点",
|
|
862
|
+
"strip.down": "向下翻出更多提问圆点",
|
|
798
863
|
"jump.inactive": "聊天视图未激活",
|
|
799
864
|
"jump.hidden": "目标无独立气泡,已定位到邻近内容",
|
|
800
865
|
"jump.notfound": "目标未加载或不存在(可能已压缩)",
|
|
@@ -803,12 +868,14 @@ window.__ModuleLoader__.load({
|
|
|
803
868
|
"settings.align.title": "导航条对齐",
|
|
804
869
|
"settings.align.desc": "选择圆点导航条锚定在对话栏的哪一侧。",
|
|
805
870
|
"settings.align.left": "左侧",
|
|
806
|
-
"settings.align.right": "右侧"
|
|
871
|
+
"settings.align.right": "右侧",
|
|
872
|
+
"settings.pagesize.title": "每次翻出数量",
|
|
873
|
+
"settings.pagesize.desc": "点击 ▲/▼ 三角按钮时,每次翻出的隐藏圆点数量。"
|
|
807
874
|
};
|
|
808
875
|
const en = {
|
|
809
876
|
"strip.empty": "No questions in this session yet",
|
|
810
|
-
"strip.up": "Reveal
|
|
811
|
-
"strip.down": "Reveal
|
|
877
|
+
"strip.up": "Reveal more question dots above",
|
|
878
|
+
"strip.down": "Reveal more question dots below",
|
|
812
879
|
"jump.inactive": "Chat view is not active",
|
|
813
880
|
"jump.hidden": "No dedicated bubble; landed on nearby content",
|
|
814
881
|
"jump.notfound": "Target not loaded or missing (maybe compacted)",
|
|
@@ -817,7 +884,9 @@ window.__ModuleLoader__.load({
|
|
|
817
884
|
"settings.align.title": "Rail alignment",
|
|
818
885
|
"settings.align.desc": "Choose which edge of the conversation column the dot rail anchors to.",
|
|
819
886
|
"settings.align.left": "Left",
|
|
820
|
-
"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."
|
|
821
890
|
};
|
|
822
891
|
//#endregion
|
|
823
892
|
//#region src/core/nodes.ts
|
|
@@ -1047,8 +1116,10 @@ window.__ModuleLoader__.load({
|
|
|
1047
1116
|
jumpToQuestion(ports, key);
|
|
1048
1117
|
},
|
|
1049
1118
|
align: () => settings.getSnapshot().align,
|
|
1050
|
-
|
|
1051
|
-
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)
|
|
1052
1123
|
};
|
|
1053
1124
|
}
|
|
1054
1125
|
/**
|