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.
- package/LICENSE +201 -0
- package/README.en.md +149 -0
- package/README.md +149 -0
- package/cordis.patch.yml +4 -0
- package/lib/client/NavOverlay.js +35 -0
- package/lib/client/NavRail.js +12 -0
- package/lib/client/QuestionNavSettingsTab.js +26 -0
- package/lib/client/QuestionNavStrip.js +555 -0
- package/lib/client/components/ConfigPanel.js +14 -0
- package/lib/client/components/DotRail.js +62 -0
- package/lib/client/components/NavOverlay.js +121 -0
- package/lib/client/components/NavRail.js +97 -0
- package/lib/client/components/RailDispatch.js +22 -0
- package/lib/client/components/RailHost.js +79 -0
- package/lib/client/components/SearchBox.js +66 -0
- package/lib/client/components/SettingsPage.js +46 -0
- package/lib/client/components/StandaloneSettings.js +16 -0
- package/lib/client/components/codex/CodexRail.js +139 -0
- package/lib/client/components/deepseek/DeepseekRail.js +264 -0
- package/lib/client/components/minimal/MinimalRail.js +135 -0
- package/lib/client/components/minimal/cards.js +32 -0
- package/lib/client/components/minimal/dot.js +11 -0
- package/lib/client/components/minimal/hint.js +30 -0
- package/lib/client/components/minimal/paging.js +5 -0
- package/lib/client/components/minimal/quick-settings.js +28 -0
- package/lib/client/components/minimal/search.js +67 -0
- package/lib/client/components/minimal/useRail.js +716 -0
- package/lib/client/components/rail/cards.js +32 -0
- package/lib/client/components/rail/hint.js +30 -0
- package/lib/client/components/rail/paging.js +5 -0
- package/lib/client/components/rail/quick-settings.js +28 -0
- package/lib/client/components/rail/search.js +76 -0
- package/lib/client/components/rail/useRail.js +845 -0
- package/lib/client/components/settings/SettingsPage.js +46 -0
- package/lib/client/components/settings/SettingsTab.js +17 -0
- package/lib/client/components/settings/StandaloneSettings.js +16 -0
- package/lib/client/components/shared/Cascade.js +26 -0
- package/lib/client/components/shared/ConfigPanel.js +27 -0
- package/lib/client/components/shared/Dot.js +8 -0
- package/lib/client/components/shared/Hint.js +5 -0
- package/lib/client/components/shared/PagingButton.js +5 -0
- package/lib/client/components/shared/RailShell.js +78 -0
- package/lib/client/components/shared/SearchBox.js +63 -0
- package/lib/client/components/shared/hooks.js +427 -0
- package/lib/client/components/shared/types.js +1 -0
- package/lib/client/config.js +1 -0
- package/lib/client/engines/jump.js +34 -0
- package/lib/client/engines/useAtBottom.js +25 -0
- package/lib/client/engines/useJump.js +40 -0
- package/lib/client/engines/useScrollSpy.js +88 -0
- package/lib/client/hooks.js +11 -0
- package/lib/client/index.js +312 -0
- package/lib/client/jump.js +255 -0
- package/lib/client/locales.js +142 -0
- package/lib/client/pure.js +61 -0
- package/lib/client/settings.js +369 -0
- package/lib/client/virtual/VirtualList.js +21 -0
- package/lib/client.js +3213 -0
- package/lib/compat.js +145 -0
- package/lib/config.js +37 -0
- package/lib/core/active-dot.js +17 -0
- package/lib/core/align.js +9 -0
- package/lib/core/config.js +37 -0
- package/lib/core/focus.js +38 -0
- package/lib/core/jump.js +98 -0
- package/lib/core/logic/active-dot.js +16 -0
- package/lib/core/logic/focus.js +34 -0
- package/lib/core/logic/jump.js +188 -0
- package/lib/core/logic/nodes.js +59 -0
- package/lib/core/model/nav-item.js +1 -0
- package/lib/core/model/turns.js +73 -0
- package/lib/core/nav-item.js +1 -0
- package/lib/core/nodes.js +57 -0
- package/lib/core/page-size.js +7 -0
- package/lib/core/question-entry.js +1 -0
- package/lib/core/time.js +23 -0
- package/lib/core/turn-dots.js +85 -0
- package/lib/core/utils/measure.js +24 -0
- package/lib/core/utils/time.js +26 -0
- package/lib/index.js +64 -0
- package/lib/nav-item.js +1 -0
- package/lib/nodes.js +59 -0
- package/lib/projection.js +126 -0
- package/lib/schema.js +26 -0
- package/lib/search.js +285 -0
- package/lib/settings.js +89 -0
- package/lib/types/client/NavOverlay.d.ts +18 -0
- package/lib/types/client/NavRail.d.ts +7 -0
- package/lib/types/client/QuestionNavSettingsTab.d.ts +16 -0
- package/lib/types/client/QuestionNavStrip.d.ts +48 -0
- package/lib/types/client/components/ConfigPanel.d.ts +14 -0
- package/lib/types/client/components/DotRail.d.ts +15 -0
- package/lib/types/client/components/NavOverlay.d.ts +30 -0
- package/lib/types/client/components/NavRail.d.ts +16 -0
- package/lib/types/client/components/RailDispatch.d.ts +8 -0
- package/lib/types/client/components/RailHost.d.ts +27 -0
- package/lib/types/client/components/SearchBox.d.ts +9 -0
- package/lib/types/client/components/SettingsPage.d.ts +9 -0
- package/lib/types/client/components/StandaloneSettings.d.ts +13 -0
- package/lib/types/client/components/codex/CodexRail.d.ts +3 -0
- package/lib/types/client/components/deepseek/DeepseekRail.d.ts +3 -0
- package/lib/types/client/components/minimal/MinimalRail.d.ts +8 -0
- package/lib/types/client/components/minimal/cards.d.ts +19 -0
- package/lib/types/client/components/minimal/dot.d.ts +14 -0
- package/lib/types/client/components/minimal/hint.d.ts +11 -0
- package/lib/types/client/components/minimal/paging.d.ts +10 -0
- package/lib/types/client/components/minimal/quick-settings.d.ts +8 -0
- package/lib/types/client/components/minimal/search.d.ts +27 -0
- package/lib/types/client/components/minimal/useRail.d.ts +90 -0
- package/lib/types/client/components/rail/cards.d.ts +19 -0
- package/lib/types/client/components/rail/hint.d.ts +11 -0
- package/lib/types/client/components/rail/paging.d.ts +10 -0
- package/lib/types/client/components/rail/quick-settings.d.ts +8 -0
- package/lib/types/client/components/rail/search.d.ts +34 -0
- package/lib/types/client/components/rail/useRail.d.ts +132 -0
- package/lib/types/client/components/settings/SettingsPage.d.ts +9 -0
- package/lib/types/client/components/settings/SettingsTab.d.ts +6 -0
- package/lib/types/client/components/settings/StandaloneSettings.d.ts +13 -0
- package/lib/types/client/components/shared/Cascade.d.ts +14 -0
- package/lib/types/client/components/shared/ConfigPanel.d.ts +8 -0
- package/lib/types/client/components/shared/Dot.d.ts +17 -0
- package/lib/types/client/components/shared/Hint.d.ts +6 -0
- package/lib/types/client/components/shared/PagingButton.d.ts +10 -0
- package/lib/types/client/components/shared/RailShell.d.ts +43 -0
- package/lib/types/client/components/shared/SearchBox.d.ts +18 -0
- package/lib/types/client/components/shared/hooks.d.ts +64 -0
- package/lib/types/client/components/shared/types.d.ts +101 -0
- package/lib/types/client/config.d.ts +9 -0
- package/lib/types/client/engines/jump.d.ts +17 -0
- package/lib/types/client/engines/useAtBottom.d.ts +1 -0
- package/lib/types/client/engines/useJump.d.ts +16 -0
- package/lib/types/client/engines/useScrollSpy.d.ts +12 -0
- package/lib/types/client/hooks.d.ts +7 -0
- package/lib/types/client/index.d.ts +31 -0
- package/lib/types/client/jump.d.ts +47 -0
- package/lib/types/client/locales.d.ts +142 -0
- package/lib/types/client/pure.d.ts +21 -0
- package/lib/types/client/settings.d.ts +173 -0
- package/lib/types/client/virtual/VirtualList.d.ts +15 -0
- package/lib/types/compat.d.ts +44 -0
- package/lib/types/config.d.ts +44 -0
- package/lib/types/core/active-dot.d.ts +15 -0
- package/lib/types/core/align.d.ts +10 -0
- package/lib/types/core/config.d.ts +44 -0
- package/lib/types/core/focus.d.ts +24 -0
- package/lib/types/core/jump.d.ts +39 -0
- package/lib/types/core/logic/active-dot.d.ts +15 -0
- package/lib/types/core/logic/focus.d.ts +22 -0
- package/lib/types/core/logic/jump.d.ts +39 -0
- package/lib/types/core/logic/nodes.d.ts +48 -0
- package/lib/types/core/model/nav-item.d.ts +12 -0
- package/lib/types/core/model/turns.d.ts +30 -0
- package/lib/types/core/nav-item.d.ts +12 -0
- package/lib/types/core/nodes.d.ts +43 -0
- package/lib/types/core/page-size.d.ts +8 -0
- package/lib/types/core/question-entry.d.ts +12 -0
- package/lib/types/core/time.d.ts +4 -0
- package/lib/types/core/turn-dots.d.ts +29 -0
- package/lib/types/core/utils/measure.d.ts +12 -0
- package/lib/types/core/utils/time.d.ts +4 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/nav-item.d.ts +12 -0
- package/lib/types/nodes.d.ts +48 -0
- package/lib/types/projection.d.ts +71 -0
- package/lib/types/schema.d.ts +37 -0
- package/lib/types/search.d.ts +42 -0
- package/lib/types/settings.d.ts +99 -0
- package/package.json +71 -0
- package/src/client/components/RailDispatch.tsx +27 -0
- package/src/client/components/codex/CodexRail.tsx +348 -0
- package/src/client/components/deepseek/DeepseekRail.tsx +522 -0
- package/src/client/components/minimal/MinimalRail.tsx +324 -0
- package/src/client/components/minimal/dot.tsx +32 -0
- package/src/client/components/rail/cards.tsx +102 -0
- package/src/client/components/rail/hint.tsx +50 -0
- package/src/client/components/rail/paging.tsx +26 -0
- package/src/client/components/rail/quick-settings.tsx +130 -0
- package/src/client/components/rail/search.tsx +147 -0
- package/src/client/components/rail/useRail.ts +906 -0
- package/src/client/components/settings/SettingsPage.tsx +198 -0
- package/src/client/components/settings/StandaloneSettings.tsx +28 -0
- package/src/client/components/shared/types.ts +99 -0
- package/src/client/index.ts +388 -0
- package/src/client/jump.ts +293 -0
- package/src/client/locales.ts +143 -0
- package/src/client/settings.ts +482 -0
- package/src/client/styles.css +1580 -0
- package/src/compat.ts +161 -0
- package/src/index.ts +66 -0
- package/src/projection.ts +174 -0
- package/src/search.ts +291 -0
- 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
|
+
}
|