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,324 @@
|
|
|
1
|
+
// 简约风格:圆点轨形态——视觉自治(轨道数据/布局/间谍/翻页/卡片能力经 ../rail 共享,
|
|
2
|
+
// 本目录仅保留风格专属部件:MinimalRail 装配 + dot 圆点按钮)
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import { useEffect, useRef, useState } from 'react'
|
|
5
|
+
import { useMarkerData, usePinning, useReadingSpy, useBandPaging, useCardPreview, useFailureNotice, useLoadingNotice, useFavorites, useMobileMode, useMobileSearchButton, useNavSettings, clockText, markerScale, markerTier, ROW_HEIGHT, usePeekState, useClosePopoversOnOutside } from '../rail/useRail.ts'
|
|
6
|
+
import { Dot } from './dot.tsx'
|
|
7
|
+
import { Cards } from '../rail/cards.tsx'
|
|
8
|
+
import { PagingButton } from '../rail/paging.tsx'
|
|
9
|
+
import { Hint } from '../rail/hint.tsx'
|
|
10
|
+
import { SearchBox } from '../rail/search.tsx'
|
|
11
|
+
import { QuickSettings } from '../rail/quick-settings.tsx'
|
|
12
|
+
import { STYLE_CAPABILITIES, BAND_HEIGHT_PX } from '../../settings.ts'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import type { RailMarker } from '../shared/types.ts'
|
|
16
|
+
import type { NavInjected, SessionListLike, Translate } from '../shared/types.ts'
|
|
17
|
+
|
|
18
|
+
export interface MinimalRailProps {
|
|
19
|
+
injected: NavInjected | undefined
|
|
20
|
+
useSessions?: <S>(selector: (s: SessionListLike) => S) => S
|
|
21
|
+
t: Translate
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function MinimalRail(props: MinimalRailProps): ReactNode {
|
|
25
|
+
const { injected, useSessions, t } = props
|
|
26
|
+
const panelRef = useRef<HTMLDivElement>(null)
|
|
27
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
28
|
+
const [jumpingKey, setJumpingKey] = useState<string | null>(null)
|
|
29
|
+
const [configOpen, setConfigOpen] = useState(false)
|
|
30
|
+
const [searchOpen, setSearchOpen] = useState(false)
|
|
31
|
+
const [favFilter, setFavFilter] = useState(false)
|
|
32
|
+
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
33
|
+
// 鼠标离开导航区域触发一次跟随:仅作 effect 重跑计数(跟随几何复用下方 effect)
|
|
34
|
+
const [followTick, setFollowTick] = useState(0)
|
|
35
|
+
const { showPeek, hidePeek, peekCls } = usePeekState()
|
|
36
|
+
// 上次 hover 命中的圆点 key(同一圆点内 mouseover 高频触发不重复开卡)
|
|
37
|
+
const lastHoverKey = useRef<string | null>(null)
|
|
38
|
+
|
|
39
|
+
// 手机模式(<1024px):仅搜索图标 + 底部半宽搜索抽屉
|
|
40
|
+
const mobile = useMobileMode()
|
|
41
|
+
|
|
42
|
+
// 数据与设置
|
|
43
|
+
const { sessionId, visible, markers } = useMarkerData(injected, useSessions)
|
|
44
|
+
const { favorites, toggle: toggleFavorite } = useFavorites(sessionId)
|
|
45
|
+
const settings = useNavSettings(injected)
|
|
46
|
+
const { style, align, bandHeight, cardCount, cardItems, showPaging } = settings
|
|
47
|
+
useMobileSearchButton(() => setDrawerOpen(true), mobile && markers.length > 0, align, t('search.placeholder'))
|
|
48
|
+
|
|
49
|
+
// 收藏过滤:派生状态——无收藏数据时自动视为未过滤
|
|
50
|
+
// (取消最后一个收藏立即恢复全部列表,不会困在空过滤态)
|
|
51
|
+
const effectiveFilter = favFilter && favorites.size > 0
|
|
52
|
+
const displayMarkers = effectiveFilter
|
|
53
|
+
? markers.filter((m) => m.members.some((k) => favorites.has(k)))
|
|
54
|
+
: markers
|
|
55
|
+
|
|
56
|
+
// 布局钉位(含导航栏偏移;sessionId 变化 → 校准循环重启,覆盖会话/工作区切换重建)
|
|
57
|
+
usePinning(panelRef, visible, align, settings.railOffset, sessionId)
|
|
58
|
+
const [currentKey, forceKey] = useReadingSpy(visible, displayMarkers)
|
|
59
|
+
const { scrollable, revealed, syncScroll, pageBy, canPageUp, canPageDown } = useBandPaging(listRef, displayMarkers)
|
|
60
|
+
|
|
61
|
+
// 悬浮卡片(风格能力)
|
|
62
|
+
const caps = STYLE_CAPABILITIES[style]
|
|
63
|
+
const focusRadius = Math.floor((cardCount - 1) / 2)
|
|
64
|
+
const { focus, openCards, cancelClear, scheduleClear } = useCardPreview(displayMarkers, align, focusRadius, caps.hoverCards)
|
|
65
|
+
|
|
66
|
+
// 跳转失败提示 / 加载较早记录提示
|
|
67
|
+
const hint = useFailureNotice(t)
|
|
68
|
+
const loading = useLoadingNotice(t)
|
|
69
|
+
|
|
70
|
+
useClosePopoversOnOutside(configOpen, searchOpen, () => {
|
|
71
|
+
setConfigOpen(false)
|
|
72
|
+
setSearchOpen(false)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// 激活节点跟随:正文滚动时导航列表平滑居中(点击跳转后不冻结)
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const list = listRef.current
|
|
78
|
+
if (list === null || currentKey === null) return
|
|
79
|
+
const index = displayMarkers.findIndex((d) => d.key === currentKey)
|
|
80
|
+
if (index < 0) return
|
|
81
|
+
const step = ROW_HEIGHT // 行距 14px(8px 节点 + 6px 间距,与 rail 层几何一致)
|
|
82
|
+
const dotTop = 8 + index * step // 列表上内边距 8px + 节点位置
|
|
83
|
+
const contentHeight = 16 + displayMarkers.length * step - 6
|
|
84
|
+
const maxScroll = Math.max(0, contentHeight - list.clientHeight)
|
|
85
|
+
const target = Math.max(0, Math.min(dotTop - list.clientHeight / 2 + step / 2, maxScroll))
|
|
86
|
+
if (Math.abs(target - list.scrollTop) < 2) return
|
|
87
|
+
list.scrollTo({ top: target, behavior: 'smooth' })
|
|
88
|
+
}, [currentKey, markers, followTick]) // followTick:鼠标离开 rail 时重跑一次跟随
|
|
89
|
+
|
|
90
|
+
if (!visible || injected === undefined || markers.length === 0) return null
|
|
91
|
+
|
|
92
|
+
const onJump = (dot: RailMarker): void => {
|
|
93
|
+
if (sessionId === undefined) return
|
|
94
|
+
setJumpingKey(dot.key)
|
|
95
|
+
forceKey(dot.key) // 点击瞬间强制激活目标(正文滚动稳定后由采样判据接管)
|
|
96
|
+
const currentMarker = currentKey === null ? undefined : displayMarkers.find((m) => m.key === currentKey)
|
|
97
|
+
injected.jump(sessionId, dot.key, dot.seq, currentMarker?.seq)
|
|
98
|
+
window.setTimeout(() => setJumpingKey((k) => (k === dot.key ? null : k)), 600)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 事件委托(圆点是 memo 纯展示):hover 开悬浮卡 / 点击跳转。
|
|
102
|
+
// 用 onMouseOver(真实冒泡,圆点间移动每次触发)而非 onMouseEnter
|
|
103
|
+
// (容器级 mouseenter 只在从外部进入时触发一次——圆点间移动不会换卡)。
|
|
104
|
+
const onDotsOver = (e: React.MouseEvent): void => {
|
|
105
|
+
const el = e.target as HTMLElement
|
|
106
|
+
const hit = el.closest<HTMLElement>('[data-mega-chat-nav-key]')
|
|
107
|
+
if (hit === null) return
|
|
108
|
+
const key = hit.dataset.megaChatNavKey
|
|
109
|
+
if (key === undefined || key === lastHoverKey.current) return
|
|
110
|
+
const dot = displayMarkers.find((d) => d.key === key)
|
|
111
|
+
if (dot === undefined) return
|
|
112
|
+
lastHoverKey.current = key
|
|
113
|
+
cancelClear()
|
|
114
|
+
openCards(dot, hit)
|
|
115
|
+
}
|
|
116
|
+
const onDotsClick = (e: React.MouseEvent): void => {
|
|
117
|
+
const el = e.target as HTMLElement
|
|
118
|
+
const hit = el.closest<HTMLElement>('[data-mega-chat-nav-key]')
|
|
119
|
+
if (hit === null) return
|
|
120
|
+
const key = hit.dataset.megaChatNavKey
|
|
121
|
+
const dot = displayMarkers.find((d) => d.key === key)
|
|
122
|
+
if (dot !== undefined) onJump(dot)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (mobile) {
|
|
126
|
+
return (
|
|
127
|
+
<>
|
|
128
|
+
{drawerOpen ? (
|
|
129
|
+
<>
|
|
130
|
+
<div className="mgcn-drawerMask" onClick={() => setDrawerOpen(false)} />
|
|
131
|
+
<div className="mgcn-searchDrawer" role="dialog" aria-label={t('search.placeholder')}>
|
|
132
|
+
<SearchBox
|
|
133
|
+
sessionId={sessionId}
|
|
134
|
+
scopes={settings.searchScopes}
|
|
135
|
+
t={t}
|
|
136
|
+
onJump={(key, seq) => {
|
|
137
|
+
setDrawerOpen(false)
|
|
138
|
+
onJump({ key, seq: seq ?? 0, turn: null, time: Date.now(), texts: [], members: [key] })
|
|
139
|
+
}}
|
|
140
|
+
onClose={() => setDrawerOpen(false)}
|
|
141
|
+
emptyContent={
|
|
142
|
+
<div className="mgcn-drawerList">
|
|
143
|
+
{[...markers].reverse().slice(0, 20).map((m) => (
|
|
144
|
+
<button
|
|
145
|
+
key={m.key}
|
|
146
|
+
type="button"
|
|
147
|
+
className="mgcn-drawerItem"
|
|
148
|
+
onClick={() => { setDrawerOpen(false); onJump(m) }}
|
|
149
|
+
>
|
|
150
|
+
<span className="mgcn-drawerItemTitle">
|
|
151
|
+
<span className="mgcn-drawerItemTurn">{m.turn !== null ? 'Turn ' + m.turn : ''}</span>
|
|
152
|
+
<span className="mgcn-drawerItemTime">{clockText(m.time, Date.now())}</span>
|
|
153
|
+
</span>
|
|
154
|
+
<span className="mgcn-drawerItemText">{m.texts[0] ?? ''}</span>
|
|
155
|
+
</button>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
</>
|
|
162
|
+
) : null}
|
|
163
|
+
</>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const selectedIndex = focus === null ? -1 : displayMarkers.findIndex((d) => d.key === focus?.key)
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
ref={panelRef}
|
|
172
|
+
className={align === 'right' ? 'mgcn-rail mgcn-railRight' : 'mgcn-rail'}
|
|
173
|
+
data-mega-chat-nav="rail"
|
|
174
|
+
onMouseEnter={showPeek}
|
|
175
|
+
onMouseLeave={() => { hidePeek(); setFollowTick((n) => n + 1) }} // 离开:触发一次跟随计算
|
|
176
|
+
>
|
|
177
|
+
<div className="mgcn-queue">
|
|
178
|
+
{displayMarkers.length === 0 ? (
|
|
179
|
+
<div className="mgcn-empty">{t('strip.empty')}</div>
|
|
180
|
+
) : (
|
|
181
|
+
<div
|
|
182
|
+
className="mgcn-list"
|
|
183
|
+
style={{ '--mgcn-band-height': BAND_HEIGHT_PX.minimal[bandHeight] + 'px' } as React.CSSProperties}
|
|
184
|
+
>
|
|
185
|
+
{/* 操作区:.mgcn-list 子级悬浮定位,不占列表高度、不混入圆点流 */}
|
|
186
|
+
<div className="mgcn-actions">
|
|
187
|
+
{settings.showQuickSettings !== 'hide' ? (
|
|
188
|
+
<span className={'mgcn-gear-wrap mgcn-gear-wrap--' + align + peekCls(settings.showQuickSettings, configOpen)}>
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className="mgcn-config-gear"
|
|
192
|
+
aria-label={t('config.title')}
|
|
193
|
+
onClick={() => { setConfigOpen((v) => !v); setSearchOpen(false) }}
|
|
194
|
+
>
|
|
195
|
+
<svg className="mgcn-btn-icon" viewBox="0 0 24 24" width="12" height="12" aria-hidden="true">
|
|
196
|
+
<circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" strokeWidth="1.7" />
|
|
197
|
+
<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" />
|
|
198
|
+
</svg>
|
|
199
|
+
</button>
|
|
200
|
+
{configOpen ? (
|
|
201
|
+
<QuickSettings injected={injected} t={t} onClose={() => setConfigOpen(false)} />
|
|
202
|
+
) : null}
|
|
203
|
+
</span>
|
|
204
|
+
) : null}
|
|
205
|
+
{settings.showFavorites !== 'hide' ? (
|
|
206
|
+
<span className={'mgcn-gear-wrap mgcn-gear-wrap--' + align + peekCls(settings.showFavorites, false)}>
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
className={effectiveFilter ? 'mgcn-config-gear mgcn-favFilterOn' : 'mgcn-config-gear'}
|
|
210
|
+
aria-label={t('fav.filter')}
|
|
211
|
+
onClick={() => { if (favorites.size > 0) setFavFilter((v) => !v) }}
|
|
212
|
+
>
|
|
213
|
+
<svg className="mgcn-btn-icon" viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
|
|
214
|
+
<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"
|
|
215
|
+
fill={favFilter ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" />
|
|
216
|
+
</svg>
|
|
217
|
+
</button>
|
|
218
|
+
</span>
|
|
219
|
+
) : null}
|
|
220
|
+
{settings.showSearch !== 'hide' ? (
|
|
221
|
+
<span className={'mgcn-gear-wrap mgcn-gear-wrap--' + align + peekCls(settings.showSearch, searchOpen)}>
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
className="mgcn-config-gear"
|
|
225
|
+
aria-label={t('search.placeholder')}
|
|
226
|
+
onClick={() => { setSearchOpen((v) => !v); setConfigOpen(false) }}
|
|
227
|
+
>
|
|
228
|
+
<svg className="mgcn-btn-icon" viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
|
|
229
|
+
<circle cx="7" cy="7" r="4.5" fill="none" stroke="currentColor" strokeWidth="1.6" />
|
|
230
|
+
<line x1="10.4" y1="10.4" x2="14" y2="14" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
|
|
231
|
+
</svg>
|
|
232
|
+
</button>
|
|
233
|
+
{searchOpen ? (
|
|
234
|
+
<SearchBox
|
|
235
|
+
sessionId={sessionId}
|
|
236
|
+
scopes={settings.searchScopes}
|
|
237
|
+
t={t}
|
|
238
|
+
onJump={(key, seq) => onJump({ key, seq: seq ?? 0, turn: null, time: Date.now(), texts: [], members: [key] })}
|
|
239
|
+
onClose={() => setSearchOpen(false)}
|
|
240
|
+
/>
|
|
241
|
+
) : null}
|
|
242
|
+
</span>
|
|
243
|
+
) : null}
|
|
244
|
+
{settings.showCount !== 'hide' ? (
|
|
245
|
+
<span className={'mgcn-count' + peekCls(settings.showCount, false)}>{displayMarkers.length}</span>
|
|
246
|
+
) : null}
|
|
247
|
+
</div>
|
|
248
|
+
{scrollable && showPaging !== 'hide' ? (
|
|
249
|
+
<div className={'mgcn-pageUp' + peekCls(showPaging, false)}>
|
|
250
|
+
<PagingButton
|
|
251
|
+
dir={-1}
|
|
252
|
+
visible={canPageUp}
|
|
253
|
+
label={t('strip.up')}
|
|
254
|
+
onEnter={cancelClear}
|
|
255
|
+
onLeave={scheduleClear}
|
|
256
|
+
onClick={() => pageBy(-1)}
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
) : null}
|
|
260
|
+
<div
|
|
261
|
+
ref={listRef}
|
|
262
|
+
className="mgcn-dots"
|
|
263
|
+
onScroll={syncScroll}
|
|
264
|
+
onMouseLeave={() => { lastHoverKey.current = null; scheduleClear() }}
|
|
265
|
+
onMouseOver={onDotsOver}
|
|
266
|
+
onClick={onDotsClick}
|
|
267
|
+
>
|
|
268
|
+
{displayMarkers.map((dot, index) => {
|
|
269
|
+
const isFocused = focus !== null && focus.key === dot.key
|
|
270
|
+
const tier = selectedIndex < 0 ? null : markerTier(index - selectedIndex)
|
|
271
|
+
const scale = jumpingKey === dot.key ? 1.6 : tier !== null ? markerScale(tier) : 1
|
|
272
|
+
const reveal = revealed?.get(dot.key)
|
|
273
|
+
const cls = ['mgcn-dot']
|
|
274
|
+
if (isFocused) cls.push('mgcn-focused')
|
|
275
|
+
if (jumpingKey === dot.key) cls.push('mgcn-active')
|
|
276
|
+
if (currentKey === dot.key) cls.push('mgcn-current')
|
|
277
|
+
if (reveal !== undefined) {
|
|
278
|
+
cls.push(reveal.dir === 1 ? 'mgcn-dotEnterFromBottom' : 'mgcn-dotEnterFromTop')
|
|
279
|
+
}
|
|
280
|
+
return (
|
|
281
|
+
<Dot
|
|
282
|
+
key={dot.key}
|
|
283
|
+
keyName={dot.key}
|
|
284
|
+
index={index}
|
|
285
|
+
className={cls.join(' ')}
|
|
286
|
+
scale={scale}
|
|
287
|
+
label={dot.texts[0] ?? ''}
|
|
288
|
+
focused={isFocused}
|
|
289
|
+
revealed={reveal}
|
|
290
|
+
/>
|
|
291
|
+
)
|
|
292
|
+
})}
|
|
293
|
+
</div>
|
|
294
|
+
{scrollable && showPaging !== 'hide' ? (
|
|
295
|
+
<div className={'mgcn-pageDown' + peekCls(showPaging, false)}>
|
|
296
|
+
<PagingButton
|
|
297
|
+
dir={1}
|
|
298
|
+
visible={canPageDown}
|
|
299
|
+
label={t('strip.down')}
|
|
300
|
+
onEnter={cancelClear}
|
|
301
|
+
onLeave={scheduleClear}
|
|
302
|
+
onClick={() => pageBy(1)}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
) : null}
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
{loading !== null ? <Hint text={loading} align={align} position="bottom" loading /> : hint !== null ? <Hint text={hint} align={align} /> : null}
|
|
310
|
+
{focus !== null ? (
|
|
311
|
+
<Cards
|
|
312
|
+
focus={focus}
|
|
313
|
+
align={align}
|
|
314
|
+
cardItems={cardItems}
|
|
315
|
+
favorites={favorites}
|
|
316
|
+
onToggleFavorite={toggleFavorite}
|
|
317
|
+
onJump={onJump}
|
|
318
|
+
onEnter={cancelClear}
|
|
319
|
+
onLeave={scheduleClear}
|
|
320
|
+
/>
|
|
321
|
+
) : null}
|
|
322
|
+
</div>
|
|
323
|
+
)
|
|
324
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// 圆点按钮:纯展示(缩放/跳转/翻页弹出状态由父级 memo 前算好)——
|
|
2
|
+
// memo 化后长会话 hover 不再整列重渲;事件由列表容器委托
|
|
3
|
+
import { memo, type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
export interface DotProps {
|
|
6
|
+
/** 缩放(聚焦模型输出;跳转中 1.6) */
|
|
7
|
+
scale: number
|
|
8
|
+
className: string
|
|
9
|
+
label: string
|
|
10
|
+
index: number
|
|
11
|
+
keyName: string
|
|
12
|
+
focused: boolean
|
|
13
|
+
revealed?: { dir: 1 | -1; order: number }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Dot = memo(function Dot(props: DotProps): ReactNode {
|
|
17
|
+
const { scale, className, label, index, keyName, focused, revealed } = props
|
|
18
|
+
return (
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
className={className}
|
|
22
|
+
style={{
|
|
23
|
+
transform: 'scale(' + scale + ')',
|
|
24
|
+
...(revealed !== undefined ? { animationDelay: revealed.order * 35 + 'ms' } : {}),
|
|
25
|
+
}}
|
|
26
|
+
data-mega-chat-nav-focused={focused ? 'true' : undefined}
|
|
27
|
+
data-mega-chat-nav-index={index}
|
|
28
|
+
data-mega-chat-nav-key={keyName}
|
|
29
|
+
aria-label={label}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// 悬浮卡片组(minimal 能力):内容块由配置多选(轮次 / 用时 / 收藏)控制
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { createPortal } from 'react-dom'
|
|
4
|
+
import type { RailAlign, CardItem } from '../../settings.ts'
|
|
5
|
+
import { cardMetrics, metricsLine, clockText } from './useRail.ts'
|
|
6
|
+
import type { FocusState, RailMarker } from '../shared/types.ts'
|
|
7
|
+
|
|
8
|
+
export interface CardsProps {
|
|
9
|
+
focus: FocusState
|
|
10
|
+
align: RailAlign
|
|
11
|
+
/** 显示哪些内容块(多选配置) */
|
|
12
|
+
cardItems: readonly CardItem[]
|
|
13
|
+
/** 收藏集合 */
|
|
14
|
+
favorites: ReadonlySet<string>
|
|
15
|
+
/** 切换收藏(按该轮首问锚点 key) */
|
|
16
|
+
onToggleFavorite: (key: string) => void
|
|
17
|
+
onJump: (dot: RailMarker) => void
|
|
18
|
+
/** 卡片 hover 保活(取消 rail 的清焦定时) */
|
|
19
|
+
onEnter: () => void
|
|
20
|
+
/** 离开卡片后重新排程清焦 */
|
|
21
|
+
onLeave: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function favoriteButton(favorited: boolean, onClick: () => void): ReactNode {
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
className={favorited ? 'mgcn-cardFav mgcn-cardFavOn' : 'mgcn-cardFav'}
|
|
29
|
+
aria-label={favorited ? 'unfavorite' : 'favorite'}
|
|
30
|
+
onClick={(e) => { e.stopPropagation(); onClick() }}
|
|
31
|
+
>
|
|
32
|
+
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true">
|
|
33
|
+
<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"
|
|
34
|
+
fill={favorited ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" />
|
|
35
|
+
</svg>
|
|
36
|
+
</button>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Cards(props: CardsProps): ReactNode {
|
|
41
|
+
const { focus, align, cardItems, favorites, onToggleFavorite, onJump, onEnter, onLeave } = props
|
|
42
|
+
const showTurn = cardItems.includes('turn')
|
|
43
|
+
const showDuration = cardItems.includes('duration')
|
|
44
|
+
const showFavorite = cardItems.includes('favorite')
|
|
45
|
+
return createPortal(
|
|
46
|
+
<div
|
|
47
|
+
className={align === 'right' ? 'mgcn-cascade mgcn-cascadeRight' : 'mgcn-cascade'}
|
|
48
|
+
style={{
|
|
49
|
+
...(focus.left !== undefined ? { left: focus.left } : {}),
|
|
50
|
+
...(focus.right !== undefined ? { right: focus.right } : {}),
|
|
51
|
+
top: focus.centerY,
|
|
52
|
+
transform: 'translateY(-50%)',
|
|
53
|
+
}}
|
|
54
|
+
onMouseEnter={onEnter}
|
|
55
|
+
onMouseLeave={onLeave}
|
|
56
|
+
>
|
|
57
|
+
{focus.items.map((item) => {
|
|
58
|
+
const metrics = cardMetrics(item.distance)
|
|
59
|
+
if (metrics === null) return null
|
|
60
|
+
const isSelected = item.distance === 0
|
|
61
|
+
const dot = item.dot
|
|
62
|
+
const text = dot.texts.join(' · ')
|
|
63
|
+
const cls = isSelected
|
|
64
|
+
? 'mgcn-card mgcn-cardSelected' + (align === 'right' ? ' mgcn-cardSelectedRight' : '')
|
|
65
|
+
: 'mgcn-card'
|
|
66
|
+
const favorited = favorites.has(dot.key)
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
key={dot.key}
|
|
70
|
+
className={cls}
|
|
71
|
+
role="button"
|
|
72
|
+
tabIndex={-1}
|
|
73
|
+
style={{ width: metrics.widthPx }}
|
|
74
|
+
onClick={() => onJump(dot)}
|
|
75
|
+
>
|
|
76
|
+
<div className="mgcn-cardHead">
|
|
77
|
+
{showFavorite ? favoriteButton(favorited, () => onToggleFavorite(dot.key)) : null}
|
|
78
|
+
{showTurn && isSelected && dot.turn !== null ? (
|
|
79
|
+
<div className="mgcn-cardTitle">Turn {dot.turn}</div>
|
|
80
|
+
) : null}
|
|
81
|
+
<div className="mgcn-cardTime">{clockText(dot.time, Date.now())}</div>
|
|
82
|
+
</div>
|
|
83
|
+
{showDuration && dot.metrics !== undefined && isSelected ? (
|
|
84
|
+
<div className="mgcn-cardMetrics">{metricsLine(dot.metrics)}</div>
|
|
85
|
+
) : null}
|
|
86
|
+
<div
|
|
87
|
+
className={isSelected ? 'mgcn-cardBody' : 'mgcn-cardClamp'}
|
|
88
|
+
style={
|
|
89
|
+
isSelected
|
|
90
|
+
? undefined
|
|
91
|
+
: { WebkitLineClamp: metrics.maxLines, fontSize: metrics.fontSize } as React.CSSProperties
|
|
92
|
+
}
|
|
93
|
+
>
|
|
94
|
+
{text}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
})}
|
|
99
|
+
</div>,
|
|
100
|
+
document.body,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// 跳转失败 / 加载提示(portal 到 body):
|
|
2
|
+
// - 失败提示:贴 rail 侧顶部(rail 左右对齐)
|
|
3
|
+
// - 加载提示:对话输入框上方细条,水平对齐相对**对话区域**左右边缘(跟随 rail 侧)
|
|
4
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
5
|
+
import { createPortal } from 'react-dom'
|
|
6
|
+
import type { RailAlign } from '../../settings.ts'
|
|
7
|
+
|
|
8
|
+
export interface HintProps {
|
|
9
|
+
text: string
|
|
10
|
+
align: RailAlign
|
|
11
|
+
/** bottom:显示在对话输入框上方(加载较早记录提示),水平对齐跟随对话区域左右边缘 */
|
|
12
|
+
position?: 'rail' | 'bottom'
|
|
13
|
+
/** 加载点动画(仅加载中提示) */
|
|
14
|
+
loading?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 对话区域左/右边缘到视口的距离(fixed 定位用) */
|
|
18
|
+
function conversationEdge(align: RailAlign): number | null {
|
|
19
|
+
const root = document.querySelector<HTMLElement>('[data-slot="conversation"] > div[data-phase]')
|
|
20
|
+
if (root === null) return null
|
|
21
|
+
const r = root.getBoundingClientRect()
|
|
22
|
+
return align === 'right' ? window.innerWidth - r.right : r.left
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Hint({ text, align, position = 'rail', loading = false }: HintProps): ReactNode {
|
|
26
|
+
const [edge, setEdge] = useState<number | null>(() => conversationEdge(align))
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const measure = (): void => setEdge(conversationEdge(align))
|
|
30
|
+
measure()
|
|
31
|
+
window.addEventListener('resize', measure)
|
|
32
|
+
return () => window.removeEventListener('resize', measure)
|
|
33
|
+
}, [align])
|
|
34
|
+
|
|
35
|
+
const cls = position === 'bottom' ? 'mgcn-hint mgcn-hint--bottom' : 'mgcn-hint'
|
|
36
|
+
const style = position === 'bottom'
|
|
37
|
+
? align === 'right'
|
|
38
|
+
? { right: (edge ?? 0) + 20, bottom: 150 }
|
|
39
|
+
: { left: (edge ?? 0) + 20, bottom: 150 }
|
|
40
|
+
: align === 'right' ? { right: 52, top: 12 } : { left: 52, top: 12 }
|
|
41
|
+
return createPortal(
|
|
42
|
+
<div className={cls} style={style}>
|
|
43
|
+
{loading ? (
|
|
44
|
+
<span className="mgcn-hint-dots" aria-hidden="true"><i /><i /><i /></span>
|
|
45
|
+
) : null}
|
|
46
|
+
<span>{text}</span>
|
|
47
|
+
</div>,
|
|
48
|
+
document.body,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// ▲/▼ 翻页按钮(minimal 与 codex 统一三角形态;显示模式由宿主配置 showPaging 控制)
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface PagingButtonProps {
|
|
5
|
+
dir: 1 | -1
|
|
6
|
+
visible: boolean
|
|
7
|
+
label: string
|
|
8
|
+
onEnter: () => void
|
|
9
|
+
onLeave: () => void
|
|
10
|
+
onClick: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PagingButton(props: PagingButtonProps): ReactNode {
|
|
14
|
+
const cls = props.dir === -1 ? 'mgcn-navBtn mgcn-navUp' : 'mgcn-navBtn mgcn-navDown'
|
|
15
|
+
return (
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
className={cls}
|
|
19
|
+
aria-label={props.label}
|
|
20
|
+
style={{ visibility: props.visible ? 'visible' : 'hidden' }}
|
|
21
|
+
onMouseEnter={props.onEnter}
|
|
22
|
+
onMouseLeave={props.onLeave}
|
|
23
|
+
onClick={props.onClick}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// 快捷设置面板:对齐/条带高度/卡片数(能力感知)+ 显示模式
|
|
2
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
3
|
+
import { ALIGN_OPTIONS, BAND_OPTIONS, CARD_OPTIONS, MARK_TONE_OPTIONS, SHOW_OPTIONS, STYLE_CAPABILITIES, type BandHeight, type CardCount, type MarkTone, type RailAlign, type ShowMode } from '../../settings.ts'
|
|
4
|
+
import type { NavInjected, Translate } from '../shared/types.ts'
|
|
5
|
+
|
|
6
|
+
export interface QuickSettingsProps {
|
|
7
|
+
injected: NavInjected
|
|
8
|
+
t: Translate
|
|
9
|
+
onClose: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function Seg<T extends string | number>(props: { options: readonly T[]; value: T; labels: Record<string, string>; onPick: (v: T) => void }): ReactNode {
|
|
13
|
+
return (
|
|
14
|
+
<div className="mgcn-segmented" role="radiogroup">
|
|
15
|
+
{props.options.map((o) => (
|
|
16
|
+
<button
|
|
17
|
+
key={o}
|
|
18
|
+
type="button"
|
|
19
|
+
role="radio"
|
|
20
|
+
aria-checked={props.value === o}
|
|
21
|
+
className={props.value === o ? 'mgcn-segment mgcn-segmentActive' : 'mgcn-segment'}
|
|
22
|
+
onClick={() => props.onPick(o)}
|
|
23
|
+
>
|
|
24
|
+
{props.labels[String(o)] ?? String(o)}
|
|
25
|
+
</button>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ModeSeg({ value, onChange, t }: { value: ShowMode; onChange(v: ShowMode): void; t: Translate }): ReactNode {
|
|
32
|
+
return (
|
|
33
|
+
<Seg
|
|
34
|
+
options={SHOW_OPTIONS}
|
|
35
|
+
value={value}
|
|
36
|
+
labels={{
|
|
37
|
+
show: t('config.showMode.show'),
|
|
38
|
+
peek: t('config.showMode.peek'),
|
|
39
|
+
hide: t('config.showMode.hide'),
|
|
40
|
+
}}
|
|
41
|
+
onPick={(v) => onChange(v)}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function useSettingsTick(subscribe: NavInjected['subscribeSettings']): void {
|
|
47
|
+
const [, bump] = useState(0)
|
|
48
|
+
useEffect(() => subscribe(() => bump((n) => n + 1)), [subscribe])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function QuickSettings(props: QuickSettingsProps): ReactNode {
|
|
52
|
+
const { injected, t, onClose } = props
|
|
53
|
+
useSettingsTick(injected.subscribeSettings)
|
|
54
|
+
const caps = STYLE_CAPABILITIES[injected.style()]
|
|
55
|
+
return (
|
|
56
|
+
<div className="mgcn-config-panel" role="dialog" aria-label={t('config.title')}>
|
|
57
|
+
<div className="mgcn-config-head">
|
|
58
|
+
<span className="mgcn-config-title">{t('config.style.' + injected.style())} · {t('config.settings')}</span>
|
|
59
|
+
<button type="button" className="mgcn-config-close" aria-label={t('config.close')} onClick={onClose}>✕</button>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="mgcn-config-row">
|
|
62
|
+
<span className="mgcn-config-label">{t('config.align')}</span>
|
|
63
|
+
<Seg
|
|
64
|
+
options={ALIGN_OPTIONS}
|
|
65
|
+
value={injected.align()}
|
|
66
|
+
labels={{ left: t('settings.align.left'), right: t('settings.align.right') }}
|
|
67
|
+
onPick={(v: RailAlign) => injected.setAlign(v)}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="mgcn-config-row">
|
|
71
|
+
<span className="mgcn-config-label">{t('config.bandHeight')}</span>
|
|
72
|
+
<Seg
|
|
73
|
+
options={BAND_OPTIONS}
|
|
74
|
+
value={injected.bandHeight()}
|
|
75
|
+
labels={{
|
|
76
|
+
compact: t('config.bandHeight.compact'),
|
|
77
|
+
standard: t('config.bandHeight.standard'),
|
|
78
|
+
tall: t('config.bandHeight.tall'),
|
|
79
|
+
}}
|
|
80
|
+
onPick={(v: BandHeight) => injected.setBandHeight(v)}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
{caps.hoverCards ? (
|
|
84
|
+
<div className="mgcn-config-row">
|
|
85
|
+
<span className="mgcn-config-label">{t('config.cardCount')}</span>
|
|
86
|
+
<Seg
|
|
87
|
+
options={CARD_OPTIONS}
|
|
88
|
+
value={injected.cardCount()}
|
|
89
|
+
labels={{ 1: '1', 3: '3', 5: '5' }}
|
|
90
|
+
onPick={(v: CardCount) => injected.setCardCount(v)}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
) : null}
|
|
94
|
+
{injected.style() === 'codex' ? (
|
|
95
|
+
<div className="mgcn-config-row">
|
|
96
|
+
<span className="mgcn-config-label">{t('config.markTone')}</span>
|
|
97
|
+
<Seg
|
|
98
|
+
options={MARK_TONE_OPTIONS}
|
|
99
|
+
value={injected.markTone()}
|
|
100
|
+
labels={{ soft: t('config.markTone.soft'), deep: t('config.markTone.deep') }}
|
|
101
|
+
onPick={(v: MarkTone) => injected.setMarkTone(v)}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
) : null}
|
|
105
|
+
<div className="mgcn-config-sep" />
|
|
106
|
+
<div className="mgcn-config-row">
|
|
107
|
+
<span className="mgcn-config-label">{t('config.showQuickSettings')}</span>
|
|
108
|
+
<ModeSeg value={injected.showQuickSettings()} onChange={(v) => injected.setShowQuickSettings(v)} t={t} />
|
|
109
|
+
</div>
|
|
110
|
+
<div className="mgcn-config-row">
|
|
111
|
+
<span className="mgcn-config-label">{t('config.showSearch')}</span>
|
|
112
|
+
<ModeSeg value={injected.showSearch()} onChange={(v) => injected.setShowSearch(v)} t={t} />
|
|
113
|
+
</div>
|
|
114
|
+
<div className="mgcn-config-row">
|
|
115
|
+
<span className="mgcn-config-label">{t('config.showFavorites')}</span>
|
|
116
|
+
<ModeSeg value={injected.showFavorites()} onChange={(v) => injected.setShowFavorites(v)} t={t} />
|
|
117
|
+
</div>
|
|
118
|
+
<div className="mgcn-config-row">
|
|
119
|
+
<span className="mgcn-config-label">{t('config.showCount')}</span>
|
|
120
|
+
<ModeSeg value={injected.showCount()} onChange={(v) => injected.setShowCount(v)} t={t} />
|
|
121
|
+
</div>
|
|
122
|
+
{STYLE_CAPABILITIES[injected.style()].paging ? (
|
|
123
|
+
<div className="mgcn-config-row">
|
|
124
|
+
<span className="mgcn-config-label">{t('config.showPaging')}</span>
|
|
125
|
+
<ModeSeg value={injected.showPaging()} onChange={(v) => injected.setShowPaging(v)} t={t} />
|
|
126
|
+
</div>
|
|
127
|
+
) : null}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|