dsh-mega-chat-nav 0.1.1

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.
Files changed (192) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +149 -0
  3. package/README.md +149 -0
  4. package/cordis.patch.yml +4 -0
  5. package/lib/client/NavOverlay.js +35 -0
  6. package/lib/client/NavRail.js +12 -0
  7. package/lib/client/QuestionNavSettingsTab.js +26 -0
  8. package/lib/client/QuestionNavStrip.js +555 -0
  9. package/lib/client/components/ConfigPanel.js +14 -0
  10. package/lib/client/components/DotRail.js +62 -0
  11. package/lib/client/components/NavOverlay.js +121 -0
  12. package/lib/client/components/NavRail.js +97 -0
  13. package/lib/client/components/RailDispatch.js +22 -0
  14. package/lib/client/components/RailHost.js +79 -0
  15. package/lib/client/components/SearchBox.js +66 -0
  16. package/lib/client/components/SettingsPage.js +46 -0
  17. package/lib/client/components/StandaloneSettings.js +16 -0
  18. package/lib/client/components/codex/CodexRail.js +139 -0
  19. package/lib/client/components/deepseek/DeepseekRail.js +264 -0
  20. package/lib/client/components/minimal/MinimalRail.js +135 -0
  21. package/lib/client/components/minimal/cards.js +32 -0
  22. package/lib/client/components/minimal/dot.js +11 -0
  23. package/lib/client/components/minimal/hint.js +30 -0
  24. package/lib/client/components/minimal/paging.js +5 -0
  25. package/lib/client/components/minimal/quick-settings.js +28 -0
  26. package/lib/client/components/minimal/search.js +67 -0
  27. package/lib/client/components/minimal/useRail.js +716 -0
  28. package/lib/client/components/rail/cards.js +32 -0
  29. package/lib/client/components/rail/hint.js +30 -0
  30. package/lib/client/components/rail/paging.js +5 -0
  31. package/lib/client/components/rail/quick-settings.js +28 -0
  32. package/lib/client/components/rail/search.js +76 -0
  33. package/lib/client/components/rail/useRail.js +845 -0
  34. package/lib/client/components/settings/SettingsPage.js +46 -0
  35. package/lib/client/components/settings/SettingsTab.js +17 -0
  36. package/lib/client/components/settings/StandaloneSettings.js +16 -0
  37. package/lib/client/components/shared/Cascade.js +26 -0
  38. package/lib/client/components/shared/ConfigPanel.js +27 -0
  39. package/lib/client/components/shared/Dot.js +8 -0
  40. package/lib/client/components/shared/Hint.js +5 -0
  41. package/lib/client/components/shared/PagingButton.js +5 -0
  42. package/lib/client/components/shared/RailShell.js +78 -0
  43. package/lib/client/components/shared/SearchBox.js +63 -0
  44. package/lib/client/components/shared/hooks.js +427 -0
  45. package/lib/client/components/shared/types.js +1 -0
  46. package/lib/client/config.js +1 -0
  47. package/lib/client/engines/jump.js +34 -0
  48. package/lib/client/engines/useAtBottom.js +25 -0
  49. package/lib/client/engines/useJump.js +40 -0
  50. package/lib/client/engines/useScrollSpy.js +88 -0
  51. package/lib/client/hooks.js +11 -0
  52. package/lib/client/index.js +312 -0
  53. package/lib/client/jump.js +255 -0
  54. package/lib/client/locales.js +142 -0
  55. package/lib/client/pure.js +61 -0
  56. package/lib/client/settings.js +369 -0
  57. package/lib/client/virtual/VirtualList.js +21 -0
  58. package/lib/client.js +3213 -0
  59. package/lib/compat.js +145 -0
  60. package/lib/config.js +37 -0
  61. package/lib/core/active-dot.js +17 -0
  62. package/lib/core/align.js +9 -0
  63. package/lib/core/config.js +37 -0
  64. package/lib/core/focus.js +38 -0
  65. package/lib/core/jump.js +98 -0
  66. package/lib/core/logic/active-dot.js +16 -0
  67. package/lib/core/logic/focus.js +34 -0
  68. package/lib/core/logic/jump.js +188 -0
  69. package/lib/core/logic/nodes.js +59 -0
  70. package/lib/core/model/nav-item.js +1 -0
  71. package/lib/core/model/turns.js +73 -0
  72. package/lib/core/nav-item.js +1 -0
  73. package/lib/core/nodes.js +57 -0
  74. package/lib/core/page-size.js +7 -0
  75. package/lib/core/question-entry.js +1 -0
  76. package/lib/core/time.js +23 -0
  77. package/lib/core/turn-dots.js +85 -0
  78. package/lib/core/utils/measure.js +24 -0
  79. package/lib/core/utils/time.js +26 -0
  80. package/lib/index.js +64 -0
  81. package/lib/nav-item.js +1 -0
  82. package/lib/nodes.js +59 -0
  83. package/lib/projection.js +126 -0
  84. package/lib/schema.js +26 -0
  85. package/lib/search.js +285 -0
  86. package/lib/settings.js +89 -0
  87. package/lib/types/client/NavOverlay.d.ts +18 -0
  88. package/lib/types/client/NavRail.d.ts +7 -0
  89. package/lib/types/client/QuestionNavSettingsTab.d.ts +16 -0
  90. package/lib/types/client/QuestionNavStrip.d.ts +48 -0
  91. package/lib/types/client/components/ConfigPanel.d.ts +14 -0
  92. package/lib/types/client/components/DotRail.d.ts +15 -0
  93. package/lib/types/client/components/NavOverlay.d.ts +30 -0
  94. package/lib/types/client/components/NavRail.d.ts +16 -0
  95. package/lib/types/client/components/RailDispatch.d.ts +8 -0
  96. package/lib/types/client/components/RailHost.d.ts +27 -0
  97. package/lib/types/client/components/SearchBox.d.ts +9 -0
  98. package/lib/types/client/components/SettingsPage.d.ts +9 -0
  99. package/lib/types/client/components/StandaloneSettings.d.ts +13 -0
  100. package/lib/types/client/components/codex/CodexRail.d.ts +3 -0
  101. package/lib/types/client/components/deepseek/DeepseekRail.d.ts +3 -0
  102. package/lib/types/client/components/minimal/MinimalRail.d.ts +8 -0
  103. package/lib/types/client/components/minimal/cards.d.ts +19 -0
  104. package/lib/types/client/components/minimal/dot.d.ts +14 -0
  105. package/lib/types/client/components/minimal/hint.d.ts +11 -0
  106. package/lib/types/client/components/minimal/paging.d.ts +10 -0
  107. package/lib/types/client/components/minimal/quick-settings.d.ts +8 -0
  108. package/lib/types/client/components/minimal/search.d.ts +27 -0
  109. package/lib/types/client/components/minimal/useRail.d.ts +90 -0
  110. package/lib/types/client/components/rail/cards.d.ts +19 -0
  111. package/lib/types/client/components/rail/hint.d.ts +11 -0
  112. package/lib/types/client/components/rail/paging.d.ts +10 -0
  113. package/lib/types/client/components/rail/quick-settings.d.ts +8 -0
  114. package/lib/types/client/components/rail/search.d.ts +34 -0
  115. package/lib/types/client/components/rail/useRail.d.ts +132 -0
  116. package/lib/types/client/components/settings/SettingsPage.d.ts +9 -0
  117. package/lib/types/client/components/settings/SettingsTab.d.ts +6 -0
  118. package/lib/types/client/components/settings/StandaloneSettings.d.ts +13 -0
  119. package/lib/types/client/components/shared/Cascade.d.ts +14 -0
  120. package/lib/types/client/components/shared/ConfigPanel.d.ts +8 -0
  121. package/lib/types/client/components/shared/Dot.d.ts +17 -0
  122. package/lib/types/client/components/shared/Hint.d.ts +6 -0
  123. package/lib/types/client/components/shared/PagingButton.d.ts +10 -0
  124. package/lib/types/client/components/shared/RailShell.d.ts +43 -0
  125. package/lib/types/client/components/shared/SearchBox.d.ts +18 -0
  126. package/lib/types/client/components/shared/hooks.d.ts +64 -0
  127. package/lib/types/client/components/shared/types.d.ts +101 -0
  128. package/lib/types/client/config.d.ts +9 -0
  129. package/lib/types/client/engines/jump.d.ts +17 -0
  130. package/lib/types/client/engines/useAtBottom.d.ts +1 -0
  131. package/lib/types/client/engines/useJump.d.ts +16 -0
  132. package/lib/types/client/engines/useScrollSpy.d.ts +12 -0
  133. package/lib/types/client/hooks.d.ts +7 -0
  134. package/lib/types/client/index.d.ts +31 -0
  135. package/lib/types/client/jump.d.ts +47 -0
  136. package/lib/types/client/locales.d.ts +142 -0
  137. package/lib/types/client/pure.d.ts +21 -0
  138. package/lib/types/client/settings.d.ts +173 -0
  139. package/lib/types/client/virtual/VirtualList.d.ts +15 -0
  140. package/lib/types/compat.d.ts +44 -0
  141. package/lib/types/config.d.ts +44 -0
  142. package/lib/types/core/active-dot.d.ts +15 -0
  143. package/lib/types/core/align.d.ts +10 -0
  144. package/lib/types/core/config.d.ts +44 -0
  145. package/lib/types/core/focus.d.ts +24 -0
  146. package/lib/types/core/jump.d.ts +39 -0
  147. package/lib/types/core/logic/active-dot.d.ts +15 -0
  148. package/lib/types/core/logic/focus.d.ts +22 -0
  149. package/lib/types/core/logic/jump.d.ts +39 -0
  150. package/lib/types/core/logic/nodes.d.ts +48 -0
  151. package/lib/types/core/model/nav-item.d.ts +12 -0
  152. package/lib/types/core/model/turns.d.ts +30 -0
  153. package/lib/types/core/nav-item.d.ts +12 -0
  154. package/lib/types/core/nodes.d.ts +43 -0
  155. package/lib/types/core/page-size.d.ts +8 -0
  156. package/lib/types/core/question-entry.d.ts +12 -0
  157. package/lib/types/core/time.d.ts +4 -0
  158. package/lib/types/core/turn-dots.d.ts +29 -0
  159. package/lib/types/core/utils/measure.d.ts +12 -0
  160. package/lib/types/core/utils/time.d.ts +4 -0
  161. package/lib/types/index.d.ts +4 -0
  162. package/lib/types/nav-item.d.ts +12 -0
  163. package/lib/types/nodes.d.ts +48 -0
  164. package/lib/types/projection.d.ts +71 -0
  165. package/lib/types/schema.d.ts +37 -0
  166. package/lib/types/search.d.ts +42 -0
  167. package/lib/types/settings.d.ts +99 -0
  168. package/package.json +71 -0
  169. package/src/client/components/RailDispatch.tsx +27 -0
  170. package/src/client/components/codex/CodexRail.tsx +348 -0
  171. package/src/client/components/deepseek/DeepseekRail.tsx +522 -0
  172. package/src/client/components/minimal/MinimalRail.tsx +324 -0
  173. package/src/client/components/minimal/dot.tsx +32 -0
  174. package/src/client/components/rail/cards.tsx +102 -0
  175. package/src/client/components/rail/hint.tsx +50 -0
  176. package/src/client/components/rail/paging.tsx +26 -0
  177. package/src/client/components/rail/quick-settings.tsx +130 -0
  178. package/src/client/components/rail/search.tsx +147 -0
  179. package/src/client/components/rail/useRail.ts +906 -0
  180. package/src/client/components/settings/SettingsPage.tsx +198 -0
  181. package/src/client/components/settings/StandaloneSettings.tsx +28 -0
  182. package/src/client/components/shared/types.ts +99 -0
  183. package/src/client/index.ts +388 -0
  184. package/src/client/jump.ts +293 -0
  185. package/src/client/locales.ts +143 -0
  186. package/src/client/settings.ts +482 -0
  187. package/src/client/styles.css +1580 -0
  188. package/src/compat.ts +161 -0
  189. package/src/index.ts +66 -0
  190. package/src/projection.ts +174 -0
  191. package/src/search.ts +291 -0
  192. package/src/settings.ts +169 -0
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // shell.overlay 浮层:解析当前会话 → 订阅投影 face → 几何钉到对话列 → 渲染圆点轨
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { useScope } from "../hooks.js";
5
+ import { useJump } from "../engines/useJump.js";
6
+ import { useScrollSpy } from "../engines/useScrollSpy.js";
7
+ import { useAtBottom } from "../engines/useAtBottom.js";
8
+ import { isChatViewVisible, hasTrajectoryView } from "../pure.js";
9
+ import { DotRail } from "./DotRail.js";
10
+ const EDGE = 12;
11
+ function currentSessionId(value) {
12
+ if (typeof value === 'string' && value.length > 0)
13
+ return value;
14
+ if (value && typeof value === 'object') {
15
+ const id = value.id;
16
+ if (typeof id === 'string' && id.length > 0)
17
+ return id;
18
+ }
19
+ return undefined;
20
+ }
21
+ /** 会话滚动容器(钉位基准) */
22
+ function hostElement() {
23
+ return (document.querySelector('[data-conversation-scroll]') ??
24
+ document.querySelector('[data-slot="conversation"]'));
25
+ }
26
+ export function NavOverlay(props) {
27
+ const cfg = useScope(props.scope);
28
+ const tr = props.t ?? ((k) => k);
29
+ const sessionId = currentSessionId(props.useSessions?.((s) => s.current));
30
+ // 投影订阅(binding.session.projections.faceOf)
31
+ const [wire, setWire] = useState(undefined);
32
+ useEffect(() => {
33
+ const face = props.sessions?.binding?.(sessionId ?? '')?.session?.projections?.faceOf?.('msgNavMessages');
34
+ if (!face) {
35
+ setWire(undefined);
36
+ return;
37
+ }
38
+ const sync = () => { setWire(face.getSnapshot()); };
39
+ sync();
40
+ return face.subscribe(sync);
41
+ }, [sessionId, props.sessions]);
42
+ const items = wire?.items ?? [];
43
+ // 几何定位:钉到对话列(含边缘偏移)
44
+ const alignRef = useRef(cfg?.align ?? 'right');
45
+ alignRef.current = cfg?.align ?? 'right';
46
+ const [pos, setPos] = useState(null);
47
+ useEffect(() => {
48
+ let raf = 0;
49
+ let stopped = false;
50
+ const measure = () => {
51
+ if (stopped)
52
+ return;
53
+ const host = hostElement();
54
+ if (!host) {
55
+ setPos(null);
56
+ return;
57
+ }
58
+ const r = host.getBoundingClientRect();
59
+ const next = { top: r.top, height: r.height };
60
+ if (alignRef.current === 'right') {
61
+ next.right = Math.max(EDGE, window.innerWidth - r.right + EDGE);
62
+ }
63
+ else {
64
+ next.left = Math.max(EDGE, r.left + EDGE);
65
+ }
66
+ setPos((prev) => prev &&
67
+ Math.abs(prev.top - next.top) < 1 &&
68
+ Math.abs(prev.height - next.height) < 1 &&
69
+ prev.left === next.left &&
70
+ prev.right === next.right
71
+ ? prev
72
+ : next);
73
+ raf = requestAnimationFrame(measure);
74
+ };
75
+ raf = requestAnimationFrame(measure);
76
+ const watch = hostElement();
77
+ let ro = null;
78
+ if (watch) {
79
+ ro = new ResizeObserver(() => { raf = requestAnimationFrame(measure); });
80
+ ro.observe(watch);
81
+ }
82
+ window.addEventListener('resize', measure);
83
+ const iv = setInterval(measure, 2000);
84
+ return () => {
85
+ stopped = true;
86
+ cancelAnimationFrame(raf);
87
+ ro?.disconnect();
88
+ window.removeEventListener('resize', measure);
89
+ clearInterval(iv);
90
+ };
91
+ }, []);
92
+ // TabGate
93
+ const [active, setActive] = useState(true);
94
+ useEffect(() => {
95
+ let stopped = false;
96
+ let raf = 0;
97
+ const check = () => {
98
+ if (stopped)
99
+ return;
100
+ const next = isChatViewVisible(document) && !hasTrajectoryView(document);
101
+ setActive((prev) => (prev === next ? prev : next));
102
+ raf = requestAnimationFrame(check);
103
+ };
104
+ raf = requestAnimationFrame(check);
105
+ return () => { stopped = true; cancelAnimationFrame(raf); };
106
+ }, []);
107
+ const keys = useMemo(() => items.filter((it) => it.role === 'user' || it.role === 'steering').map((it) => it.key), [items]);
108
+ const atBottom = useAtBottom();
109
+ const activeKey = useScrollSpy({
110
+ rule: cfg?.anchorRule ?? 'line40',
111
+ keys,
112
+ enabled: active && keys.length > 0,
113
+ atBottom: () => atBottom,
114
+ });
115
+ const { failed, jump } = useJump(cfg?.scrollBehavior ?? 'smooth', cfg?.pulseMs ?? 1200, {
116
+ getSession: () => props.sessions?.binding?.(sessionId ?? '')?.session,
117
+ });
118
+ if (!cfg?.enabled || !active)
119
+ return null;
120
+ return (_jsx(DotRail, { items: items, align: cfg.align ?? 'right', cfg: cfg, t: tr, activeKey: activeKey, failed: failed, onJump: (k) => { void jump(k); }, pos: pos }));
121
+ }
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // 简约导航条(参考 question-nav 形态):竖排圆点迷你图 + 悬停预览卡 + 滚动高亮 + 跳转
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { useScope } from "../hooks.js";
5
+ import { useJump } from "../engines/useJump.js";
6
+ import { isChatViewVisible, hasTrajectoryView } from "../pure.js";
7
+ import { VirtualList } from "../virtual/VirtualList.js";
8
+ import { useScrollSpy } from "../engines/useScrollSpy.js";
9
+ import { useAtBottom } from "../engines/useAtBottom.js";
10
+ import { formatTime, truncate } from "../pure.js";
11
+ /** 圆点行高:8px 点 + 6px 间距 */
12
+ const DOT_STEP = 14;
13
+ /** 悬停波纹缩放(选中点与上下邻居逐级):FOCUS_RADIUS=2 */
14
+ const RIPPLE = [2.0, 1.55, 1.25];
15
+ function rippleScale(dist) {
16
+ if (dist < RIPPLE.length)
17
+ return RIPPLE[dist];
18
+ return 1;
19
+ }
20
+ /** 圆点轨条目:用户/steering 主消息(assistant 不进圆点轨) */
21
+ function userItems(items) {
22
+ return items.filter((it) => it.role === 'user' || it.role === 'steering');
23
+ }
24
+ export function NavRail(props) {
25
+ const cfg = useScope(props.scope);
26
+ const wire = props.useProjection?.('msgNavMessages');
27
+ const all = wire?.items ?? [];
28
+ const items = useMemo(() => userItems(all), [all]);
29
+ const [scrollTop, setScrollTop] = useState(0);
30
+ const [viewportH, setViewportH] = useState(360);
31
+ const [hoverKey, setHoverKey] = useState(null);
32
+ const bodyRef = useRef(null);
33
+ const atBottom = useAtBottom();
34
+ const tr = props.t ?? ((k) => k);
35
+ const enabled = cfg?.enabled ?? true;
36
+ const align = cfg?.align ?? 'right';
37
+ // TabGate:chat 视图可见(防御 trajectory)
38
+ const [active, setActive] = useState(true);
39
+ useEffect(() => {
40
+ let stopped = false;
41
+ let raf = 0;
42
+ const check = () => {
43
+ if (stopped)
44
+ return;
45
+ const next = isChatViewVisible(document) && !hasTrajectoryView(document);
46
+ setActive((prev) => (prev === next ? prev : next));
47
+ raf = requestAnimationFrame(check);
48
+ };
49
+ raf = requestAnimationFrame(check);
50
+ return () => { stopped = true; cancelAnimationFrame(raf); };
51
+ }, []);
52
+ // 视口高度跟随容器
53
+ useEffect(() => {
54
+ const el = bodyRef.current;
55
+ if (!el)
56
+ return;
57
+ const ro = new ResizeObserver(() => {
58
+ const h = el.clientHeight;
59
+ setViewportH((prev) => (Math.abs(prev - h) > 1 ? h : prev));
60
+ });
61
+ ro.observe(el);
62
+ setViewportH(el.clientHeight || 360);
63
+ return () => ro.disconnect();
64
+ }, []);
65
+ const keys = items.map((it) => it.key);
66
+ const activeKey = useScrollSpy({
67
+ rule: cfg?.anchorRule ?? 'line40',
68
+ keys,
69
+ enabled: items.length > 0 && active,
70
+ atBottom: () => atBottom,
71
+ });
72
+ const { failed, jump } = useJump(cfg?.scrollBehavior ?? 'smooth', cfg?.pulseMs ?? 1200, {
73
+ getSession: () => {
74
+ const b = props.sessions?.binding?.(props.sessionId ?? '');
75
+ return b?.session;
76
+ },
77
+ });
78
+ const hoverIdx = hoverKey ? keys.indexOf(hoverKey) : -1;
79
+ const hovered = hoverIdx >= 0 ? items[hoverIdx] : undefined;
80
+ const pinned = useMemo(() => {
81
+ const s = new Set();
82
+ if (activeKey)
83
+ s.add(activeKey);
84
+ if (hoverKey)
85
+ s.add(hoverKey);
86
+ return s.size > 0 ? s : undefined;
87
+ }, [activeKey, hoverKey]);
88
+ if (!enabled || !active)
89
+ return null;
90
+ return (_jsxs("div", { className: 'mgs-dots mgs-dots--' + align, role: "navigation", "aria-label": tr('a11y.list'), children: [_jsx("div", { className: "mgs-dots__track", ref: bodyRef, children: items.length === 0 ? (_jsx("div", { className: "mgs-dots__empty", children: tr('empty') })) : (_jsx(VirtualList, { items: items, itemHeight: DOT_STEP, viewportHeight: viewportH, scrollTop: scrollTop, onScrollTopChange: setScrollTop, keyOf: (it) => it.key, pinned: pinned, renderItem: (it, i) => {
91
+ const dist = hoverIdx >= 0 ? Math.abs(i - hoverIdx) : 99;
92
+ const scale = rippleScale(dist);
93
+ return (_jsx("button", { type: "button", className: 'mgs-dot' +
94
+ (it.key === activeKey ? ' mgs-dot--active' : '') +
95
+ (it.key === hoverKey ? ' mgs-dot--hover' : ''), style: { transform: 'scale(' + scale + ')' }, "aria-label": truncate(it.text, 40), "aria-current": it.key === activeKey ? 'step' : undefined, onClick: () => { void jump(it.key); }, onMouseEnter: () => setHoverKey(it.key), onMouseLeave: () => setHoverKey((k) => (k === it.key ? null : k)) }));
96
+ } })) }), hovered && (_jsxs("div", { className: 'mgs-dots__tip mgs-dots__tip--' + align, children: [_jsx("div", { className: "mgs-dots__tip-text", children: truncate(hovered.text, 80) || tr('empty') }), _jsx("div", { className: "mgs-dots__tip-time", children: formatTime(hovered.time) })] })), failed && _jsx("div", { className: "mgs-dots__hint", children: tr('jump.failed') })] }));
97
+ }
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // 风格分发:按当前配置的 style 挂载对应风格轨组件(minimal 圆点轨 / codex 刻度轨)。
3
+ // 风格配置各自独立(styles.<style> 段),此处仅做选择——切换风格即卸载/重挂风格轨。
4
+ import { useEffect, useState } from 'react';
5
+ import { DEFAULT_STYLE } from "../settings.js";
6
+ import { MinimalRail } from "./minimal/MinimalRail.js";
7
+ import { CodexRail } from "./codex/CodexRail.js";
8
+ import { DeepseekRail } from "./deepseek/DeepseekRail.js";
9
+ export function RailDispatch(props) {
10
+ const [style, setStyle] = useState(() => props.injected?.style() ?? DEFAULT_STYLE);
11
+ useEffect(() => {
12
+ if (props.injected === undefined)
13
+ return;
14
+ return props.injected.subscribeSettings(() => setStyle(props.injected?.style() ?? DEFAULT_STYLE));
15
+ }, [props.injected]);
16
+ const railProps = { injected: props.injected, useSessions: props.useSessions, t: props.t };
17
+ if (style === 'codex')
18
+ return _jsx(CodexRail, { ...railProps });
19
+ if (style === 'deepseek')
20
+ return _jsx(DeepseekRail, { ...railProps });
21
+ return _jsx(MinimalRail, { ...railProps });
22
+ }
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // 导航条宿主:TabGate 守卫 + 折叠/对齐/风格分发 + 搜索/配置入口 + 共用跳转
3
+ import { useEffect, useState } from 'react';
4
+ import { useScope } from "../hooks.js";
5
+ import { useJump } from "../engines/useJump.js";
6
+ import { isChatViewVisible, hasTrajectoryView } from "../pure.js";
7
+ import { SearchBox } from "./SearchBox.js";
8
+ import { ConfigPanel } from "./ConfigPanel.js";
9
+ import { MinimalRail } from "./minimal/MinimalRail.js";
10
+ import { DeepseekRail } from "./deepseek/DeepseekRail.js";
11
+ import { CodexRail } from "./codex/CodexRail.js";
12
+ export function RailHost(props) {
13
+ const cfg = useScope(props.scope);
14
+ const wire = props.useProjection?.('msgNavMessages');
15
+ const items = wire?.items ?? [];
16
+ const [collapsed, setCollapsed] = useState(false);
17
+ const [searchOpen, setSearchOpen] = useState(false);
18
+ const [configOpen, setConfigOpen] = useState(false);
19
+ const [active, setActive] = useState(true);
20
+ const tr = props.t ?? ((k) => k);
21
+ // TabGate:chat 视图可见(conversation.input.dock 已限 chat 域,此处防御 trajectory)
22
+ useEffect(() => {
23
+ let stopped = false;
24
+ let raf = 0;
25
+ const check = () => {
26
+ if (stopped)
27
+ return;
28
+ const next = isChatViewVisible(document) && !hasTrajectoryView(document);
29
+ setActive((prev) => (prev === next ? prev : next));
30
+ raf = requestAnimationFrame(check);
31
+ };
32
+ raf = requestAnimationFrame(check);
33
+ return () => { stopped = true; cancelAnimationFrame(raf); };
34
+ }, []);
35
+ // Esc 收起(a11yShortcuts 开启时)
36
+ useEffect(() => {
37
+ if (!cfg?.general.a11yShortcuts)
38
+ return;
39
+ const onKey = (e) => {
40
+ if (e.key === 'Escape') {
41
+ setSearchOpen(false);
42
+ setConfigOpen(false);
43
+ setCollapsed(true);
44
+ }
45
+ };
46
+ document.addEventListener('keydown', onKey);
47
+ return () => document.removeEventListener('keydown', onKey);
48
+ }, [cfg?.general.a11yShortcuts]);
49
+ const { failed, jump } = useJump(cfg?.general.scrollBehavior ?? 'smooth', cfg?.general.pulseMs ?? 1200, {
50
+ getSession: () => {
51
+ const b = props.sessions?.binding?.(props.sessionId ?? '');
52
+ return b?.session;
53
+ },
54
+ });
55
+ if (!cfg || !active || !cfg.general.enabled)
56
+ return null;
57
+ const g = cfg.general;
58
+ const style = g.style;
59
+ const common = {
60
+ items,
61
+ align: g.align,
62
+ collapsed,
63
+ setCollapsed,
64
+ t: tr,
65
+ general: g,
66
+ jump,
67
+ };
68
+ let body;
69
+ switch (style) {
70
+ case 'deepseek':
71
+ body = _jsx(DeepseekRail, { ...common, cfg: cfg.styles.deepseek });
72
+ break;
73
+ case 'codex':
74
+ body = _jsx(CodexRail, { ...common, cfg: cfg.styles.codex });
75
+ break;
76
+ default: body = _jsx(MinimalRail, { ...common, cfg: cfg.styles.minimal });
77
+ }
78
+ return (_jsxs("div", { className: 'mgs-rail mgs-rail--' + g.align, role: "navigation", "aria-label": tr('a11y.list'), children: [_jsxs("div", { className: "mgs-rail__bar", children: [_jsx("button", { type: "button", className: "mgs-rail__toggle", onClick: () => setCollapsed((v) => !v), "aria-label": tr(collapsed ? 'collapse.expand' : 'collapse.collapse'), children: collapsed ? '›' : '‹' }), !collapsed && g.showSearch && (_jsx("button", { type: "button", className: "mgs-rail__toggle", "aria-label": tr('a11y.search'), onClick: () => setSearchOpen((v) => !v), children: "\uD83D\uDD0D" })), _jsx("button", { type: "button", className: "mgs-rail__toggle", "aria-label": tr('settings.entry'), onClick: () => setConfigOpen((v) => !v), children: "\u2699" })] }), !collapsed && body, !collapsed && failed && _jsx("div", { className: "mgs-rail__hint", children: tr('jump.failed') }), !collapsed && configOpen && (_jsx("div", { className: "mgs-rail__search", children: _jsx(ConfigPanel, { scope: props.scope, t: tr, onClose: () => setConfigOpen(false) }) })), !collapsed && searchOpen && (_jsx("div", { className: "mgs-rail__search", children: _jsx(SearchBox, { sessionId: props.sessionId, debounceMs: g.searchDebounceMs, t: tr, onJump: (key) => { void jump(key); setSearchOpen(false); }, onClose: () => setSearchOpen(false) }) }))] }));
79
+ }
@@ -0,0 +1,66 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // 搜索浮层:防抖 + AbortController + 结果渲染(命中词高亮)+ 点击直达
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { formatTime } from "../pure.js";
5
+ function HighlightedSnippet({ hit }) {
6
+ if (hit.hitStart < 0 || hit.hitLen <= 0)
7
+ return _jsx(_Fragment, { children: hit.snippet });
8
+ const before = hit.snippet.slice(0, hit.hitStart);
9
+ const mid = hit.snippet.slice(hit.hitStart, hit.hitStart + hit.hitLen);
10
+ const after = hit.snippet.slice(hit.hitStart + hit.hitLen);
11
+ return (_jsxs(_Fragment, { children: [before, _jsx("mark", { className: "mgs-search__mark", children: mid }), after] }));
12
+ }
13
+ export function SearchBox(props) {
14
+ const [q, setQ] = useState('');
15
+ const [results, setResults] = useState([]);
16
+ const [busy, setBusy] = useState(false);
17
+ const [empty, setEmpty] = useState(false);
18
+ const abortRef = useRef(null);
19
+ const timerRef = useRef(0);
20
+ const inputRef = useRef(null);
21
+ const run = useCallback((query) => {
22
+ abortRef.current?.abort();
23
+ const ac = new AbortController();
24
+ abortRef.current = ac;
25
+ const trimmed = query.trim();
26
+ if (trimmed.length < 1) {
27
+ setResults([]);
28
+ setEmpty(false);
29
+ setBusy(false);
30
+ return;
31
+ }
32
+ setBusy(true);
33
+ const url = '/mega-chat-nav/search?session=' + encodeURIComponent(props.sessionId ?? '') +
34
+ '&q=' + encodeURIComponent(trimmed) + '&limit=50';
35
+ fetch(url, { signal: ac.signal })
36
+ .then((r) => r.json())
37
+ .then((data) => {
38
+ if (!data.ok) {
39
+ setResults([]);
40
+ setEmpty(false);
41
+ return;
42
+ }
43
+ const hits = data.hits ?? [];
44
+ setResults(hits);
45
+ setEmpty(hits.length === 0);
46
+ })
47
+ .catch((e) => {
48
+ if (e.name !== 'AbortError') {
49
+ setResults([]);
50
+ setEmpty(false);
51
+ }
52
+ })
53
+ .finally(() => setBusy(false));
54
+ }, [props.sessionId]);
55
+ const onChange = (v) => {
56
+ setQ(v);
57
+ clearTimeout(timerRef.current);
58
+ timerRef.current = setTimeout(() => run(v), props.debounceMs);
59
+ };
60
+ useEffect(() => {
61
+ inputRef.current?.focus();
62
+ return () => { clearTimeout(timerRef.current); abortRef.current?.abort(); };
63
+ }, []);
64
+ return (_jsxs("div", { className: "mgs-search", role: "dialog", "aria-label": props.t('search.placeholder'), children: [_jsx("input", { ref: inputRef, className: "mgs-search__input", value: q, placeholder: props.t('search.placeholder'), onChange: (e) => onChange(e.target.value), onKeyDown: (e) => { if (e.key === 'Escape')
65
+ props.onClose(); } }), busy && _jsx("div", { className: "mgs-search__busy", children: "\u2026" }), _jsxs("div", { className: "mgs-search__results", children: [empty && _jsx("div", { className: "mgs-search__empty", children: props.t('search.empty') }), results.map((hit) => (_jsxs("button", { type: "button", className: "mgs-search__hit", onClick: () => props.onJump(hit.key), children: [_jsx("span", { className: 'mgs-search__role mgs-search__role--' + hit.role, "aria-hidden": "true" }), _jsx("span", { className: "mgs-search__text", children: _jsx(HighlightedSnippet, { hit: hit }) }), _jsx("span", { className: "mgs-search__time", children: formatTime(hit.time) })] }, hit.key + ':' + hit.seq)))] })] }));
66
+ }
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // 完整配置页:全部配置项 + 导出/导入(mega-settings 收纳页 / 自足入口共用)
3
+ import { useState } from 'react';
4
+ import { readConfig, setGeneral, setStyleConfig, exportConfig, importConfig } from "../config.js";
5
+ import { Switch } from "./ConfigPanel.js";
6
+ const STYLES = ['minimal', 'deepseek', 'codex'];
7
+ const ANCHORS = ['line40', 'top', 'center', 'lastInteraction'];
8
+ export function SettingsPage(props) {
9
+ const scope = props.scope;
10
+ const cfg = readConfig(scope);
11
+ const [ioText, setIoText] = useState('');
12
+ const [ioError, setIoError] = useState(null);
13
+ const [ioOk, setIoOk] = useState(false);
14
+ const tr = props.t ?? ((k) => k);
15
+ if (props.hosted) {
16
+ return _jsx("div", { className: "mgs-panel mgs-config", children: tr('settings.hosted') });
17
+ }
18
+ if (!scope) {
19
+ return _jsx("div", { className: "mgs-panel mgs-config", children: tr('preview.fallback') });
20
+ }
21
+ if (!cfg) {
22
+ return _jsx("div", { className: "mgs-panel mgs-config", children: tr('settings.unavailable') });
23
+ }
24
+ const g = cfg.general;
25
+ const setG = (patch) => void setGeneral(scope, patch);
26
+ const setS = (style, patch) => void setStyleConfig(scope, style, patch);
27
+ const doExport = () => { setIoText(exportConfig(cfg)); setIoError(null); setIoOk(false); };
28
+ const doImport = async () => {
29
+ const err = await importConfig(scope, ioText);
30
+ if (err) {
31
+ setIoError(err);
32
+ setIoOk(false);
33
+ }
34
+ else {
35
+ setIoError(null);
36
+ setIoOk(true);
37
+ }
38
+ };
39
+ return (_jsxs("div", { className: "mgs-panel mgs-settings", children: [_jsx("div", { className: "mgs-panel__head", children: _jsx("span", { className: "mgs-panel__title", children: tr('settings.entry') }) }), _jsx(Field, { label: tr('config.enabled'), children: _jsx(Switch, { checked: g.enabled, onChange: (v) => setG({ enabled: v }) }) }), _jsx(Field, { label: tr('config.style'), children: _jsx(Seg, { options: STYLES.map((s) => ({ value: s, label: tr('config.style.' + s) })), value: g.style, onChange: (v) => setG({ style: v }) }) }), _jsx(Field, { label: tr('config.align'), children: _jsx(Seg, { options: ['left', 'right'].map((a) => ({ value: a, label: tr('config.align.' + a) })), value: g.align, onChange: (v) => setG({ align: v }) }) }), _jsx(Field, { label: tr('config.panelWidth'), children: _jsx("input", { className: "mgs-input mgs-input--num", type: "number", min: 200, max: 400, value: g.panelWidth, onChange: (e) => setG({ panelWidth: Math.min(400, Math.max(200, Number(e.target.value) || 280)) }) }) }), _jsx(Field, { label: tr('config.collapsed'), children: _jsx(Switch, { checked: g.collapsed, onChange: (v) => setG({ collapsed: v }) }) }), _jsx(Field, { label: tr('config.themeMode'), children: _jsx(Seg, { options: ['follow', 'custom'].map((m) => ({ value: m, label: tr('config.themeMode.' + m) })), value: g.themeMode, onChange: (v) => setG({ themeMode: v }) }) }), _jsx(Field, { label: tr('config.showSearch'), children: _jsx(Switch, { checked: g.showSearch, onChange: (v) => setG({ showSearch: v }) }) }), _jsx(Field, { label: tr('config.searchDebounceMs'), children: _jsx("input", { className: "mgs-input mgs-input--num", type: "number", min: 50, max: 2000, value: g.searchDebounceMs, onChange: (e) => setG({ searchDebounceMs: Math.min(2000, Math.max(50, Number(e.target.value) || 200)) }) }) }), _jsx(Field, { label: tr('config.anchorRule'), children: _jsx(Seg, { options: ANCHORS.map((a) => ({ value: a, label: tr('config.anchorRule.' + a) })), value: g.anchorRule, onChange: (v) => setG({ anchorRule: v }) }) }), _jsx(Field, { label: tr('config.followNewMessage'), children: _jsx(Switch, { checked: g.followNewMessage, onChange: (v) => setG({ followNewMessage: v }) }) }), _jsx(Field, { label: tr('config.scrollBehavior'), children: _jsx(Seg, { options: ['smooth', 'instant'].map((s) => ({ value: s, label: tr('config.scrollBehavior.' + s) })), value: g.scrollBehavior, onChange: (v) => setG({ scrollBehavior: v }) }) }), _jsx(Field, { label: tr('config.pulseMs'), children: _jsx("input", { className: "mgs-input mgs-input--num", type: "number", min: 200, max: 5000, value: g.pulseMs, onChange: (e) => setG({ pulseMs: Math.min(5000, Math.max(200, Number(e.target.value) || 1200)) }) }) }), _jsx(Field, { label: tr('config.summaryLength'), children: _jsx("input", { className: "mgs-input mgs-input--num", type: "number", min: 20, max: 80, value: g.summaryLength, onChange: (e) => setG({ summaryLength: Math.min(80, Math.max(20, Number(e.target.value) || 60)) }) }) }), _jsx(Field, { label: tr('config.hoverPreview'), children: _jsx(Switch, { checked: g.hoverPreview, onChange: (v) => setG({ hoverPreview: v }) }) }), _jsx("div", { className: "mgs-panel__sub", children: tr('settings.style.minimal') }), _jsx(Field, { label: tr('config.density'), children: _jsx(Seg, { options: ['compact', 'normal', 'loose'].map((d) => ({ value: d, label: tr('config.density.' + d) })), value: cfg.styles.minimal.density, onChange: (v) => setS('minimal', { density: v }) }) }), _jsx(Field, { label: tr('config.showTime'), children: _jsx(Switch, { checked: cfg.styles.minimal.showTime, onChange: (v) => setS('minimal', { showTime: v }) }) }), _jsx(Field, { label: tr('config.showSeq'), children: _jsx(Switch, { checked: cfg.styles.minimal.showSeq, onChange: (v) => setS('minimal', { showSeq: v }) }) }), _jsx("div", { className: "mgs-panel__sub", children: tr('settings.style.deepseek') }), _jsx(Field, { label: tr('config.ds.pillWidth'), children: _jsx("input", { className: "mgs-input mgs-input--num", type: "number", value: cfg.styles.deepseek.pillWidth, onChange: (e) => setS('deepseek', { pillWidth: Number(e.target.value) || 34 }) }) }), _jsx(Field, { label: tr('config.ds.panelWidth'), children: _jsx("input", { className: "mgs-input mgs-input--num", type: "number", value: cfg.styles.deepseek.panelWidth, onChange: (e) => setS('deepseek', { panelWidth: Number(e.target.value) || 272 }) }) }), _jsx(Field, { label: tr('config.ds.barGap'), children: _jsx("input", { className: "mgs-input mgs-input--num", type: "number", value: cfg.styles.deepseek.barGap, onChange: (e) => setS('deepseek', { barGap: Number(e.target.value) || 30 }) }) }), _jsx(Field, { label: tr('config.ds.hoverExpand'), children: _jsx(Switch, { checked: cfg.styles.deepseek.hoverExpand, onChange: (v) => setS('deepseek', { hoverExpand: v }) }) }), _jsx("div", { className: "mgs-panel__sub", children: tr('settings.style.codex') }), _jsx(Field, { label: tr('config.cx.tickStep'), children: _jsx("input", { className: "mgs-input mgs-input--num", type: "number", value: cfg.styles.codex.tickStep, onChange: (e) => setS('codex', { tickStep: Number(e.target.value) || 10 }) }) }), _jsx(Field, { label: tr('config.cx.fadeHeight'), children: _jsx("input", { className: "mgs-input mgs-input--num", type: "number", value: cfg.styles.codex.fadeHeight, onChange: (e) => setS('codex', { fadeHeight: Number(e.target.value) || 40 }) }) }), _jsx(Field, { label: tr('config.cx.hoverPreview'), children: _jsx(Switch, { checked: cfg.styles.codex.hoverPreview, onChange: (v) => setS('codex', { hoverPreview: v }) }) }), _jsx("div", { className: "mgs-panel__sub", children: tr('settings.io') }), _jsxs("div", { className: "mgs-row mgs-row--io", children: [_jsx("button", { type: "button", className: "mgs-btn", onClick: doExport, children: tr('settings.export') }), _jsx("button", { type: "button", className: "mgs-btn", onClick: doImport, children: tr('settings.import') })] }), _jsx("textarea", { className: "mgs-input mgs-input--io", rows: 5, placeholder: tr('settings.io.hint'), value: ioText, onChange: (e) => setIoText(e.target.value) }), ioError && _jsxs("div", { className: "mgs-rail__hint", children: [tr('settings.io.error'), ": ", ioError] }), ioOk && _jsx("div", { className: "mgs-settings__ok", children: tr('settings.io.ok') })] }));
40
+ }
41
+ function Field({ label, children }) {
42
+ return (_jsxs("div", { className: "mgs-row", children: [_jsx("span", { className: "mgs-row__label", children: label }), children] }));
43
+ }
44
+ function Seg(props) {
45
+ return (_jsx("div", { className: "mgs-seg", children: props.options.map((o) => (_jsx("button", { type: "button", className: 'mgs-seg__item' + (props.value === o.value ? ' mgs-seg__item--active' : ''), onClick: () => props.onChange(o.value), children: o.label }, o.value))) }));
46
+ }
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // 自足完整配置入口:宿主(mega-settings)已收纳时让位提示
3
+ import { useEffect, useState } from 'react';
4
+ import { SettingsPage } from "./SettingsPage.js";
5
+ export function StandaloneSettings(props) {
6
+ const [hosted, setHosted] = useState(false);
7
+ useEffect(() => {
8
+ const check = () => {
9
+ const n = props.slots?.entries?.('mega.settings.member')?.length ?? 0;
10
+ setHosted(n > 0);
11
+ };
12
+ check();
13
+ return props.slots?.subscribe?.('mega.settings.member', check);
14
+ }, [props.slots]);
15
+ return _jsx(SettingsPage, { scope: props.scope, t: props.t ?? ((k) => k), hosted: hosted });
16
+ }
@@ -0,0 +1,139 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { memo, useEffect, useRef, useState } from 'react';
3
+ import { useMarkerData, usePinning, useReadingSpy, useBandPaging, useCardPreview, useFailureNotice, useLoadingNotice, useFavorites, useMobileMode, useMobileSearchButton, useNavSettings, clockText, ROW_HEIGHT, usePeekState, useClosePopoversOnOutside } from "../rail/useRail.js";
4
+ import { Cards } from "../rail/cards.js";
5
+ import { PagingButton } from "../rail/paging.js";
6
+ import { Hint } from "../rail/hint.js";
7
+ import { SearchBox } from "../rail/search.js";
8
+ import { QuickSettings } from "../rail/quick-settings.js";
9
+ import { STYLE_CAPABILITIES, BAND_HEIGHT_PX } from "../../settings.js";
10
+ /** 刻度槽组件:纯展示(hover 波纹由 CSS 级联完成,React 不参与逐槽状态)——
11
+ * memo 化后只有 current/jumping/focused/reveal 变化才重渲,长会话 hover 不再整列刷新 */
12
+ const CodexSlot = memo(function CodexSlot(props) {
13
+ const { marker, current, focused, jumping, label, reveal } = props;
14
+ const cls = ['mgcn-cxSlot'];
15
+ if (focused)
16
+ cls.push('mgcn-cxFocused');
17
+ if (current)
18
+ cls.push('mgcn-cxCurrent');
19
+ if (jumping)
20
+ cls.push('mgcn-cxJump');
21
+ if (reveal !== undefined)
22
+ cls.push(reveal.dir === 1 ? 'mgcn-cxEnterFromBottom' : 'mgcn-cxEnterFromTop');
23
+ return (_jsx("button", { type: "button", className: cls.join(' '), style: reveal !== undefined ? { animationDelay: reveal.order * 35 + 'ms' } : undefined, "data-mega-chat-nav-key": marker.key, "aria-label": label, children: _jsx("i", { className: "mgcn-cxMark", "aria-hidden": "true" }) }));
24
+ });
25
+ /** 阅读位置跟随:正文滚动时带内刻度平滑居中(点击跳转后不冻结) */
26
+ function useFollowCurrent(listRef, currentKey, markers, resetKey) {
27
+ useEffect(() => {
28
+ const list = listRef.current;
29
+ if (list === null || currentKey === null)
30
+ return;
31
+ const index = markers.findIndex((d) => d.key === currentKey);
32
+ if (index < 0)
33
+ return;
34
+ const slotTop = 8 + index * ROW_HEIGHT;
35
+ const contentHeight = 16 + markers.length * ROW_HEIGHT;
36
+ const maxScroll = Math.max(0, contentHeight - list.clientHeight);
37
+ const target = Math.max(0, Math.min(slotTop - list.clientHeight / 2 + ROW_HEIGHT / 2, maxScroll));
38
+ if (Math.abs(target - list.scrollTop) < 2)
39
+ return;
40
+ list.scrollTo({ top: target, behavior: 'smooth' });
41
+ // resetKey:鼠标离开导航区域时 bump → effect 重跑一次跟随
42
+ }, [currentKey, markers, resetKey]);
43
+ }
44
+ export function CodexRail(props) {
45
+ const { injected, useSessions, t } = props;
46
+ const panelRef = useRef(null);
47
+ const listRef = useRef(null);
48
+ const [jumpingKey, setJumpingKey] = useState(null);
49
+ const [configOpen, setConfigOpen] = useState(false);
50
+ const [searchOpen, setSearchOpen] = useState(false);
51
+ const [favFilter, setFavFilter] = useState(false);
52
+ const [drawerOpen, setDrawerOpen] = useState(false);
53
+ // 鼠标离开导航区域触发一次跟随:仅作 effect 重跑计数
54
+ const [followTick, setFollowTick] = useState(0);
55
+ const { showPeek, hidePeek, peekCls } = usePeekState();
56
+ // 上次 hover 命中的槽 key(同一槽内 mouseover 高频触发不重复开卡)
57
+ const lastHoverKey = useRef(null);
58
+ const mobile = useMobileMode();
59
+ const { sessionId, visible, markers } = useMarkerData(injected, useSessions);
60
+ const { favorites, toggle: toggleFavorite } = useFavorites(sessionId);
61
+ const settings = useNavSettings(injected);
62
+ const { style, align, bandHeight, cardCount, cardItems, markTone, showPaging } = settings;
63
+ useMobileSearchButton(() => setDrawerOpen(true), mobile && markers.length > 0, align, t('search.placeholder'));
64
+ const effectiveFilter = favFilter && favorites.size > 0;
65
+ const displayMarkers = effectiveFilter
66
+ ? markers.filter((m) => m.members.some((k) => favorites.has(k)))
67
+ : markers;
68
+ usePinning(panelRef, visible, align, settings.railOffset, sessionId);
69
+ const [currentKey, forceKey] = useReadingSpy(visible, displayMarkers);
70
+ const { scrollable, revealed, syncScroll, pageBy, canPageUp, canPageDown } = useBandPaging(listRef, displayMarkers);
71
+ const caps = STYLE_CAPABILITIES[style];
72
+ const focusRadius = Math.floor((cardCount - 1) / 2);
73
+ const { focus, openCards, cancelClear, scheduleClear } = useCardPreview(displayMarkers, align, focusRadius, caps.hoverCards);
74
+ const hint = useFailureNotice(t);
75
+ const loading = useLoadingNotice(t);
76
+ useClosePopoversOnOutside(configOpen, searchOpen, () => {
77
+ setConfigOpen(false);
78
+ setSearchOpen(false);
79
+ });
80
+ useFollowCurrent(listRef, currentKey, displayMarkers, followTick);
81
+ if (!visible || injected === undefined || markers.length === 0)
82
+ return null;
83
+ const onJump = (slot) => {
84
+ if (sessionId === undefined)
85
+ return;
86
+ setJumpingKey(slot.key);
87
+ forceKey(slot.key);
88
+ const currentMarker = currentKey === null ? undefined : displayMarkers.find((m) => m.key === currentKey);
89
+ injected.jump(sessionId, slot.key, slot.seq, currentMarker?.seq);
90
+ window.setTimeout(() => setJumpingKey((k) => (k === slot.key ? null : k)), 600);
91
+ };
92
+ if (mobile) {
93
+ return (_jsx(_Fragment, { children: drawerOpen ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mgcn-drawerMask", onClick: () => setDrawerOpen(false) }), _jsx("div", { className: "mgcn-searchDrawer", role: "dialog", "aria-label": t('search.placeholder'), children: _jsx(SearchBox, { sessionId: sessionId, scopes: settings.searchScopes, t: t, onJump: (key, seq) => {
94
+ setDrawerOpen(false);
95
+ onJump({ key, seq: seq ?? 0, turn: null, time: Date.now(), texts: [], members: [key] });
96
+ }, onClose: () => setDrawerOpen(false), emptyContent: _jsx("div", { className: "mgcn-drawerList", children: [...markers].reverse().slice(0, 20).map((m) => (_jsxs("button", { type: "button", className: "mgcn-drawerItem", onClick: () => { setDrawerOpen(false); onJump(m); }, children: [_jsxs("span", { className: "mgcn-drawerItemTitle", children: [_jsx("span", { className: "mgcn-drawerItemTurn", children: m.turn !== null ? 'Turn ' + m.turn : '' }), _jsx("span", { className: "mgcn-drawerItemTime", children: clockText(m.time, Date.now()) })] }), _jsx("span", { className: "mgcn-drawerItemText", children: m.texts[0] ?? '' })] }, m.key))) }) }) })] })) : null }));
97
+ }
98
+ const hoverOff = () => {
99
+ lastHoverKey.current = null;
100
+ scheduleClear();
101
+ };
102
+ // 事件委托(槽是 memo 纯展示,事件挂在列表容器上):
103
+ // - 用 onMouseOver(真实冒泡,槽间移动每次触发)而非 onMouseEnter
104
+ // (容器级 mouseenter 只在从外部进入时触发一次,槽间移动不会重开卡)
105
+ // - hover 命中槽 → 打开/切换到该槽的悬浮卡(波纹由 CSS :hover 完成)
106
+ // - click 命中槽 → 跳转
107
+ const onListOver = (e) => {
108
+ const el = e.target;
109
+ const slot = el.closest('[data-mega-chat-nav-key]');
110
+ if (slot === null)
111
+ return;
112
+ const key = slot.dataset.megaChatNavKey;
113
+ if (key === undefined || key === lastHoverKey.current)
114
+ return;
115
+ const marker = displayMarkers.find((m) => m.key === key);
116
+ if (marker === undefined)
117
+ return;
118
+ lastHoverKey.current = key;
119
+ cancelClear();
120
+ openCards(marker, slot);
121
+ };
122
+ const onListClick = (e) => {
123
+ const el = e.target;
124
+ const slot = el.closest('[data-mega-chat-nav-key]');
125
+ if (slot === null)
126
+ return;
127
+ const key = slot.dataset.megaChatNavKey;
128
+ const marker = displayMarkers.find((m) => m.key === key);
129
+ if (marker !== undefined)
130
+ onJump(marker);
131
+ };
132
+ return (_jsxs("div", { ref: panelRef, className: align === 'right' ? 'mgcn-rail mgcn-cxRail mgcn-railRight' : 'mgcn-rail mgcn-cxRail', "data-mega-chat-nav": "rail", "data-tone": markTone, onMouseEnter: showPeek, onMouseLeave: () => { hidePeek(); setFollowTick((n) => n + 1); }, children: [_jsx("div", { className: "mgcn-queue", children: displayMarkers.length === 0 ? (_jsx("div", { className: "mgcn-empty", children: t('strip.empty') })) : (_jsxs("div", { className: 'mgcn-list' + (scrollable && showPaging !== 'hide' ? '' : ' mgcn-listNoPage'), style: { '--mgcn-band-height': BAND_HEIGHT_PX.codex[bandHeight] + 'px' }, children: [_jsxs("div", { className: "mgcn-actions", children: [settings.showQuickSettings !== 'hide' ? (_jsxs("span", { className: 'mgcn-gear-wrap mgcn-gear-wrap--' + align + peekCls(settings.showQuickSettings, configOpen), children: [_jsx("button", { type: "button", className: "mgcn-config-gear", "aria-label": t('config.title'), onClick: () => { setConfigOpen((v) => !v); setSearchOpen(false); }, children: _jsxs("svg", { className: "mgcn-btn-icon", viewBox: "0 0 24 24", width: "12", height: "12", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "3", fill: "none", stroke: "currentColor", strokeWidth: "1.7" }), _jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round" })] }) }), configOpen ? (_jsx(QuickSettings, { injected: injected, t: t, onClose: () => setConfigOpen(false) })) : null] })) : null, settings.showFavorites !== 'hide' ? (_jsx("span", { className: 'mgcn-gear-wrap mgcn-gear-wrap--' + align + peekCls(settings.showFavorites, false), children: _jsx("button", { type: "button", className: effectiveFilter ? 'mgcn-config-gear mgcn-favFilterOn' : 'mgcn-config-gear', "aria-label": t('fav.filter'), onClick: () => { if (favorites.size > 0)
133
+ setFavFilter((v) => !v); }, children: _jsx("svg", { className: "mgcn-btn-icon", viewBox: "0 0 16 16", width: "12", height: "12", "aria-hidden": "true", children: _jsx("path", { d: "M8 2.2l1.76 3.56 3.93.57-2.84 2.77.67 3.91L8 11.4l-3.52 1.85.67-3.91-2.84-2.77 3.93-.57z", fill: favFilter ? 'currentColor' : 'none', stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }) }) }) })) : null, settings.showSearch !== 'hide' ? (_jsxs("span", { className: 'mgcn-gear-wrap mgcn-gear-wrap--' + align + peekCls(settings.showSearch, searchOpen), children: [_jsx("button", { type: "button", className: "mgcn-config-gear", "aria-label": t('search.placeholder'), onClick: () => { setSearchOpen((v) => !v); setConfigOpen(false); }, children: _jsxs("svg", { className: "mgcn-btn-icon", viewBox: "0 0 16 16", width: "12", height: "12", "aria-hidden": "true", children: [_jsx("circle", { cx: "7", cy: "7", r: "4.5", fill: "none", stroke: "currentColor", strokeWidth: "1.6" }), _jsx("line", { x1: "10.4", y1: "10.4", x2: "14", y2: "14", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" })] }) }), searchOpen ? (_jsx(SearchBox, { sessionId: sessionId, scopes: settings.searchScopes, t: t, onJump: (key, seq) => onJump({ key, seq: seq ?? 0, turn: null, time: Date.now(), texts: [], members: [key] }), onClose: () => setSearchOpen(false) })) : null] })) : null, settings.showCount !== 'hide' ? (_jsx("span", { className: 'mgcn-count' + peekCls(settings.showCount, false), children: displayMarkers.length })) : null] }), scrollable && showPaging !== 'hide' ? (_jsx("div", { className: 'mgcn-pageUp' + peekCls(showPaging, false), children: _jsx(PagingButton, { dir: -1, visible: canPageUp, label: t('strip.up'), onEnter: cancelClear, onLeave: hoverOff, onClick: () => pageBy(-1) }) })) : null, _jsx("div", { ref: listRef, className: "mgcn-cxList", onScroll: syncScroll, onMouseLeave: hoverOff, onMouseOver: onListOver, onClick: onListClick, children: displayMarkers.map((slot) => {
134
+ const current = currentKey === slot.key;
135
+ const focused = focus !== null && focus.key === slot.key;
136
+ const reveal = revealed?.get(slot.key);
137
+ return (_jsx(CodexSlot, { marker: slot, current: current, focused: focused, jumping: jumpingKey === slot.key, label: slot.texts[0] ?? '', reveal: reveal }, slot.key));
138
+ }) }), scrollable && showPaging !== 'hide' ? (_jsx("div", { className: 'mgcn-pageDown' + peekCls(showPaging, false), children: _jsx(PagingButton, { dir: 1, visible: canPageDown, label: t('strip.down'), onEnter: cancelClear, onLeave: hoverOff, onClick: () => pageBy(1) }) })) : null] })) }), loading !== null ? _jsx(Hint, { text: loading, align: align, position: "bottom", loading: true }) : hint !== null ? _jsx(Hint, { text: hint, align: align }) : null, focus !== null ? (_jsx(Cards, { focus: focus, align: align, cardItems: cardItems, favorites: favorites, onToggleFavorite: toggleFavorite, onJump: onJump, onEnter: cancelClear, onLeave: hoverOff })) : null] }));
139
+ }