@ash-qw/dsh-theme-prts 0.1.91
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 +24 -0
- package/README.en.md +103 -0
- package/README.md +107 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +7466 -0
- package/lib/index.js +3 -0
- package/package.json +64 -0
- package/src/assets/faction-emblems/lungmen.png +0 -0
- package/src/assets/faction-emblems/penguin-logistics.png +0 -0
- package/src/assets/faction-emblems/reunion.png +0 -0
- package/src/assets/faction-emblems/rhine-lab.png +0 -0
- package/src/assets/faction-emblems/rhodes-island.png +0 -0
- package/src/assets/prts-original-avatar.png +0 -0
- package/src/assets/rhodes-dsh.svg +6 -0
- package/src/assets/topographic-map.svg +72 -0
- package/src/assets/topographic-tech.svg +19 -0
- package/src/client/assistant-glass-adapter.js +231 -0
- package/src/client/composer-glass-adapter.js +210 -0
- package/src/client/conversation-control-adapter.js +109 -0
- package/src/client/conversation-scale-adapter.js +2243 -0
- package/src/client/facility-geometry.js +60 -0
- package/src/client/facility-vector.js +230 -0
- package/src/client/floating-glass-adapter.js +177 -0
- package/src/client/host-geometry-adapter.js +123 -0
- package/src/client/index.js +359 -0
- package/src/client/layout-stability-diagnostics.js +162 -0
- package/src/client/operations-shell-v2.js +248 -0
- package/src/client/particle-field-adapter.js +1840 -0
- package/src/client/preferences.js +239 -0
- package/src/client/rc7-adapter.js +83 -0
- package/src/client/resize-shield-adapter.js +64 -0
- package/src/client/settings-page.js +62 -0
- package/src/client/sidebar-control-adapter.js +317 -0
- package/src/client/startup-sequence.js +366 -0
- package/src/client/status-projection.js +79 -0
- package/src/client/terrain-motion-adapter.js +188 -0
- package/src/client/theme-controller-v2.js +134 -0
- package/src/client/theme-controller.js +119 -0
- package/src/client/theme-settings-workbench.js +583 -0
- package/src/client/to-bottom-adapter.js +85 -0
- package/src/client/ui-store.js +23 -0
- package/src/index.js +4 -0
- package/src/styles/conversation-scale.css +471 -0
- package/src/styles/facility-vector.css +292 -0
- package/src/styles/prts.css +1223 -0
- package/src/styles/refinement.css +429 -0
- package/src/styles/stabilization.css +161 -0
- package/src/styles/startup-sequence.css +496 -0
- package/src/styles/theme-settings-workbench.css +803 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT, CONVERSATION_SCALE_FOCUS_CONTRAST_MAX, CONVERSATION_SCALE_FOCUS_CONTRAST_MIN, CONVERSATION_SCALE_FOCUS_CONTRAST_STEP, resolveParticleDetail } from './preferences.js'
|
|
2
|
+
import { resolveConversationScaleLengths } from './conversation-scale-adapter.js'
|
|
3
|
+
|
|
4
|
+
const SETTINGS_FOCUSABLE = [
|
|
5
|
+
'button:not([disabled])',
|
|
6
|
+
'input:not([disabled])',
|
|
7
|
+
'summary',
|
|
8
|
+
'[href]',
|
|
9
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
10
|
+
].join(',')
|
|
11
|
+
|
|
12
|
+
const PRESET_OPTIONS = [
|
|
13
|
+
['standard-tactical', '标准战术', '完整底纹 / 标准玻璃 / 系统动效'],
|
|
14
|
+
['clear-glass', '清晰玻璃', '克制底纹 / 清晰玻璃 / 系统动效'],
|
|
15
|
+
['quiet-reading', '静谧阅读', '克制底纹 / 柔和玻璃 / 减少动效'],
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const COMMON_CONTROLS = [
|
|
19
|
+
['texture', '环境底纹', [['off', '关闭'], ['restrained', '克制'], ['full', '完整']]],
|
|
20
|
+
['glass', '玻璃材质', [['off', '关闭'], ['soft', '柔和'], ['standard', '标准'], ['clear', '清晰']]],
|
|
21
|
+
['motion', '动态效果', [['system', '跟随系统'], ['reduced', '减少']]],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const PARTICLE_OPTIONS = [
|
|
25
|
+
['compact', '精简', 7],
|
|
26
|
+
['standard', '标准', 12],
|
|
27
|
+
['precise', '精细', 18],
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
function settingButtons(key, label, options) {
|
|
31
|
+
const note = key === 'glass'
|
|
32
|
+
? '<small data-prts-setting-note data-prts-transparency-status hidden>系统当前要求降低透明度</small>'
|
|
33
|
+
: key === 'motion'
|
|
34
|
+
? '<small data-prts-setting-note data-prts-effective-motion hidden>系统当前要求减少动态</small>'
|
|
35
|
+
: ''
|
|
36
|
+
const sample = value => key === 'texture' || key === 'glass'
|
|
37
|
+
? `<i data-prts-option-sample="${key}" data-prts-option-sample-value="${value}" aria-hidden="true"></i>`
|
|
38
|
+
: ''
|
|
39
|
+
return `<div data-prts-setting-row="${key}">
|
|
40
|
+
<span><strong>${label}</strong>${note}</span>
|
|
41
|
+
<div role="group" aria-label="${label}">${options.map(([value, text]) => `<button type="button" data-prts-setting-key="${key}" data-prts-setting-value="${value}">${sample(value)}<span>${text}</span></button>`).join('')}</div>
|
|
42
|
+
</div>`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function presetMarkup() {
|
|
46
|
+
return PRESET_OPTIONS.map(([value, title, description]) => `<button type="button" data-prts-setting-key="preset" data-prts-setting-value="${value}" data-prts-preset-card>
|
|
47
|
+
<strong>${title}</strong><span>${description}</span>
|
|
48
|
+
</button>`).join('')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function particleMarkup() {
|
|
52
|
+
return PARTICLE_OPTIONS.map(([value, label, count]) => `<button type="button" role="radio" data-prts-setting-key="particleDetail" data-prts-setting-value="${value}">
|
|
53
|
+
<i data-prts-particle-detail-preview aria-hidden="true">${Array.from({ length: count }, () => '<b></b>').join('')}</i><span>${label}</span>
|
|
54
|
+
</button>`).join('')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function calibrationMarkup() {
|
|
58
|
+
return `<div data-prts-scale-calibration data-prts-calibration-mode="hidden" data-prts-calibration-reason="unmeasured">
|
|
59
|
+
<div data-prts-scale-calibration-viewport>
|
|
60
|
+
<div data-prts-scale-calibration-canvas aria-hidden="true">
|
|
61
|
+
<i data-prts-scale-calibration-sidebar></i>
|
|
62
|
+
<span data-prts-scale-calibration-measure><i></i><output data-prts-scale-calibration-value>96 px</output></span>
|
|
63
|
+
<span data-prts-scale-calibration-rail>${Array.from({ length: 9 }, (_, index) => `<i style="--prts-calibration-tick:${index}"></i>`).join('')}</span>
|
|
64
|
+
<i data-prts-scale-calibration-gap></i>
|
|
65
|
+
<span data-prts-scale-calibration-content><i></i><small>会话内容区</small></span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div data-prts-scale-calibration-meta>
|
|
69
|
+
<span data-prts-scale-calibration-scale hidden>缩放预览</span>
|
|
70
|
+
<output data-prts-scale-calibration-status aria-live="polite">当前视图:几何测量中</output>
|
|
71
|
+
</div>
|
|
72
|
+
</div>`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function themeSettingsMarkup() {
|
|
76
|
+
return `<div data-prts-settings-backdrop hidden aria-hidden="true"></div>
|
|
77
|
+
<dialog id="prts-theme-settings" data-prts-theme-settings aria-label="P.R.T.S. 主题设置" aria-hidden="true">
|
|
78
|
+
<div data-prts-settings-frame>
|
|
79
|
+
<header data-prts-theme-settings-header>
|
|
80
|
+
<span><small>P.R.T.S. / 外观</small><strong>主题设置</strong></span>
|
|
81
|
+
<div data-prts-settings-summary aria-live="polite">
|
|
82
|
+
<span data-prts-settings-preset-status>标准战术</span>
|
|
83
|
+
<span data-prts-persistence-status hidden></span>
|
|
84
|
+
</div>
|
|
85
|
+
<button type="button" data-prts-theme-settings-close aria-label="关闭主题设置">×</button>
|
|
86
|
+
</header>
|
|
87
|
+
|
|
88
|
+
<div data-prts-theme-settings-scroll>
|
|
89
|
+
<section data-prts-settings-presets>
|
|
90
|
+
<header><span><small>快速配置</small><strong>视觉预设</strong></span></header>
|
|
91
|
+
<div>${presetMarkup()}</div>
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<section data-prts-settings-common>
|
|
95
|
+
<header><span><small>常用设置</small><strong>主题表现</strong></span></header>
|
|
96
|
+
<div data-prts-settings-common-grid>${COMMON_CONTROLS.map(([key, label, options]) => settingButtons(key, label, options)).join('')}</div>
|
|
97
|
+
</section>
|
|
98
|
+
|
|
99
|
+
<details data-prts-settings-advanced>
|
|
100
|
+
<summary><span><small>精细调整</small><strong>粒子与会话刻度</strong></span><output data-prts-advanced-summary>标准 / 96 px</output></summary>
|
|
101
|
+
<div data-prts-settings-advanced-content>
|
|
102
|
+
<section data-prts-particle-detail>
|
|
103
|
+
<header><strong>粒子精度</strong><span>同时调整会话与新会话徽记</span></header>
|
|
104
|
+
<div role="radiogroup" aria-label="粒子精度">${particleMarkup()}</div>
|
|
105
|
+
<span data-prts-particle-error role="alert" hidden>粒子资源加载异常,主题已使用静态后备效果</span>
|
|
106
|
+
</section>
|
|
107
|
+
<section data-prts-scale-adjustment>
|
|
108
|
+
<header><strong>会话刻度</strong><span>调整位置距离与悬停焦点的视觉区分</span></header>
|
|
109
|
+
${calibrationMarkup()}
|
|
110
|
+
<label data-prts-setting-row="conversationScaleMaxDistance" data-prts-setting-range-row>
|
|
111
|
+
<span>最大距离</span>
|
|
112
|
+
<div><input type="range" min="16" max="240" step="8" value="96" data-prts-setting-range data-prts-setting-key="conversationScaleMaxDistance" aria-label="会话刻度最大距离"><output data-prts-scale-distance-output>96 px</output></div>
|
|
113
|
+
</label>
|
|
114
|
+
<label data-prts-setting-row="conversationScaleFocusContrast" data-prts-setting-range-row>
|
|
115
|
+
<span>聚焦区分度</span>
|
|
116
|
+
<div data-prts-scale-focus-control>
|
|
117
|
+
<span data-prts-scale-focus-preview aria-hidden="true">${Array.from({ length: 5 }, (_, index) => `<i data-prts-scale-focus-preview-tick="${index}"></i>`).join('')}</span>
|
|
118
|
+
<input type="range" min="0" max="100" step="10" value="70" data-prts-setting-range data-prts-setting-key="conversationScaleFocusContrast" aria-label="会话刻度聚焦区分度">
|
|
119
|
+
<output data-prts-scale-focus-output>70</output>
|
|
120
|
+
</div>
|
|
121
|
+
</label>
|
|
122
|
+
</section>
|
|
123
|
+
</div>
|
|
124
|
+
</details>
|
|
125
|
+
|
|
126
|
+
<footer data-prts-settings-actions>
|
|
127
|
+
<div data-prts-persistence-error hidden role="alert">
|
|
128
|
+
<span><strong>仅本次会话生效</strong><small>无法写入本地设置,当前视觉效果仍会保留。</small></span>
|
|
129
|
+
<button type="button" data-prts-retry-save>重试保存</button>
|
|
130
|
+
</div>
|
|
131
|
+
<div data-prts-reset-zone>
|
|
132
|
+
<span><strong>恢复主题默认</strong><small>保留主题开关和启动动画设置</small></span>
|
|
133
|
+
<button type="button" data-prts-reset-visual>恢复默认</button>
|
|
134
|
+
<div data-prts-reset-confirm hidden role="group" aria-label="确认恢复主题默认">
|
|
135
|
+
<span>确认恢复全部主题参数?</span>
|
|
136
|
+
<button type="button" data-prts-reset-confirm-action>确认恢复</button>
|
|
137
|
+
<button type="button" data-prts-reset-cancel>取消</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</footer>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</dialog>`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createThemeSettingsOverlay({
|
|
147
|
+
document,
|
|
148
|
+
trigger,
|
|
149
|
+
panel,
|
|
150
|
+
backdrop,
|
|
151
|
+
onOpen = () => {},
|
|
152
|
+
onPreferenceChange = () => {},
|
|
153
|
+
onResetVisual = () => {},
|
|
154
|
+
onRetrySave = () => {},
|
|
155
|
+
getConversationScalePreview = () => ({ visible: false, reason: 'unmeasured', mode: 'hidden' }),
|
|
156
|
+
}) {
|
|
157
|
+
const window = document?.defaultView
|
|
158
|
+
const transparencyMedia = window?.matchMedia?.('(prefers-reduced-transparency: reduce)')
|
|
159
|
+
const motionMedia = window?.matchMedia?.('(prefers-reduced-motion: reduce)')
|
|
160
|
+
const nativeModal = typeof panel?.showModal === 'function' && typeof panel?.close === 'function'
|
|
161
|
+
let isOpen = false
|
|
162
|
+
let previousFocus
|
|
163
|
+
let currentPreferences
|
|
164
|
+
let currentStatus
|
|
165
|
+
let currentPersistence = { phase: 'idle', revision: 0 }
|
|
166
|
+
let scaleDistanceDraft
|
|
167
|
+
let scaleDistanceDirty = false
|
|
168
|
+
let scaleFocusContrastDraft
|
|
169
|
+
let scaleFocusContrastDirty = false
|
|
170
|
+
let calibrationResizeObserver
|
|
171
|
+
let calibrationSettleTimer
|
|
172
|
+
let calibrationFrame
|
|
173
|
+
let calibrationViewportWidth
|
|
174
|
+
let calibrationFactor
|
|
175
|
+
let savedTimer
|
|
176
|
+
let feedbackTimer
|
|
177
|
+
let feedbackTarget
|
|
178
|
+
let renderedPersistenceRevision = -1
|
|
179
|
+
|
|
180
|
+
function focusable() {
|
|
181
|
+
return [...(panel?.querySelectorAll?.(SETTINGS_FOCUSABLE) ?? [])].filter(node => !node.hidden && !node.disabled && node.closest('[hidden]') === null)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function reducedMotion() {
|
|
185
|
+
return document?.documentElement?.dataset?.prtsMotion === 'reduced' || motionMedia?.matches
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function showPanel() {
|
|
189
|
+
if (nativeModal) {
|
|
190
|
+
if (!panel.open) panel.showModal()
|
|
191
|
+
backdrop.hidden = true
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
panel.setAttribute('open', '')
|
|
195
|
+
panel.setAttribute('data-prts-dialog-fallback', '')
|
|
196
|
+
backdrop.hidden = false
|
|
197
|
+
backdrop.setAttribute('data-prts-settings-visible', '')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function hidePanel() {
|
|
201
|
+
if (nativeModal) {
|
|
202
|
+
if (panel.open) panel.close()
|
|
203
|
+
} else {
|
|
204
|
+
panel.removeAttribute('open')
|
|
205
|
+
panel.removeAttribute('data-prts-dialog-fallback')
|
|
206
|
+
backdrop.removeAttribute('data-prts-settings-visible')
|
|
207
|
+
backdrop.hidden = true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function setResetConfirmation(visible) {
|
|
212
|
+
const confirmation = panel?.querySelector?.('[data-prts-reset-confirm]')
|
|
213
|
+
const reset = panel?.querySelector?.('[data-prts-reset-visual]')
|
|
214
|
+
if (confirmation) confirmation.hidden = !visible
|
|
215
|
+
if (reset) reset.hidden = visible
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function setOpen(next, restoreFocus = true) {
|
|
219
|
+
if (!panel || !backdrop || !trigger) return false
|
|
220
|
+
const value = Boolean(next)
|
|
221
|
+
if (value === isOpen) return isOpen
|
|
222
|
+
if (!value) {
|
|
223
|
+
commitScaleDistance()
|
|
224
|
+
commitScaleFocusContrast()
|
|
225
|
+
}
|
|
226
|
+
isOpen = value
|
|
227
|
+
trigger.setAttribute('aria-expanded', String(isOpen))
|
|
228
|
+
document.documentElement.toggleAttribute('data-prts-settings-open', isOpen)
|
|
229
|
+
panel.setAttribute('aria-hidden', String(!isOpen))
|
|
230
|
+
if (isOpen) {
|
|
231
|
+
previousFocus = document.activeElement
|
|
232
|
+
onOpen()
|
|
233
|
+
showPanel()
|
|
234
|
+
panel.setAttribute('data-prts-settings-visible', '')
|
|
235
|
+
renderScaleCalibration()
|
|
236
|
+
;(panel.querySelector('[data-prts-theme-settings-close]') ?? focusable()[0] ?? panel).focus?.()
|
|
237
|
+
} else {
|
|
238
|
+
panel.removeAttribute('data-prts-settings-visible')
|
|
239
|
+
hidePanel()
|
|
240
|
+
setResetConfirmation(false)
|
|
241
|
+
if (restoreFocus) (trigger.isConnected ? trigger : previousFocus)?.focus?.()
|
|
242
|
+
}
|
|
243
|
+
return isOpen
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizeScaleDistance(value) {
|
|
247
|
+
const numeric = Number(value)
|
|
248
|
+
if (!Number.isFinite(numeric)) return 96
|
|
249
|
+
return Math.min(240, Math.max(16, Math.round(numeric / 8) * 8))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function currentScaleDistance() {
|
|
253
|
+
return normalizeScaleDistance(scaleDistanceDirty ? scaleDistanceDraft : currentPreferences?.conversationScaleMaxDistance)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizeScaleFocusContrast(value) {
|
|
257
|
+
const numeric = Number(value)
|
|
258
|
+
if (!Number.isFinite(numeric)) return CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT
|
|
259
|
+
const clamped = Math.min(CONVERSATION_SCALE_FOCUS_CONTRAST_MAX, Math.max(CONVERSATION_SCALE_FOCUS_CONTRAST_MIN, numeric))
|
|
260
|
+
return Math.round(clamped / CONVERSATION_SCALE_FOCUS_CONTRAST_STEP) * CONVERSATION_SCALE_FOCUS_CONTRAST_STEP
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function currentScaleFocusContrast() {
|
|
264
|
+
return normalizeScaleFocusContrast(scaleFocusContrastDirty ? scaleFocusContrastDraft : currentPreferences?.conversationScaleFocusContrast)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function calibrationStatus(state, value) {
|
|
268
|
+
if (state?.mode === 'configured') return `当前视图:使用配置上限 / ${Math.round(Number(state.left) || value)} px`
|
|
269
|
+
if (state?.simulated) return `模拟布局:使用配置上限 / ${value} px`
|
|
270
|
+
if (state?.mode === 'centered') return `当前视图:自动居中 / 距侧栏 ${Math.round(Number(state.left) || 0)} px`
|
|
271
|
+
const reasons = {
|
|
272
|
+
drawer: '抽屉布局', phone: '手机布局', turns: '消息不足', corridor: '会话走廊不足',
|
|
273
|
+
preview: '会话预览空间不足', unmeasured: '模拟布局',
|
|
274
|
+
}
|
|
275
|
+
return `当前视图:隐藏 / ${reasons[state?.reason] || '空间不足'}`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function setText(node, value) {
|
|
279
|
+
if (node && node.textContent !== value) node.textContent = value
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function setCalibrationResizeState(active) {
|
|
283
|
+
for (const node of [panel, backdrop]) {
|
|
284
|
+
if (!node || node.hasAttribute('data-prts-resizing') === active) continue
|
|
285
|
+
node.toggleAttribute('data-prts-resizing', active)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function cancelCalibrationFrame() {
|
|
290
|
+
if (calibrationFrame === undefined) return
|
|
291
|
+
window?.cancelAnimationFrame?.(calibrationFrame)
|
|
292
|
+
window?.clearTimeout?.(calibrationFrame)
|
|
293
|
+
calibrationFrame = undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function measureScaleCalibration() {
|
|
297
|
+
calibrationFrame = undefined
|
|
298
|
+
if (!isOpen) return
|
|
299
|
+
const viewport = panel?.querySelector?.('[data-prts-scale-calibration-viewport]')
|
|
300
|
+
const canvas = panel?.querySelector?.('[data-prts-scale-calibration-canvas]')
|
|
301
|
+
const calibration = panel?.querySelector?.('[data-prts-scale-calibration]')
|
|
302
|
+
const scaleNote = panel?.querySelector?.('[data-prts-scale-calibration-scale]')
|
|
303
|
+
if (!viewport || !canvas || !calibration) return
|
|
304
|
+
const available = calibrationViewportWidth || viewport.clientWidth
|
|
305
|
+
const factor = available > 0 ? Math.min(1, available / 360) : 1
|
|
306
|
+
const roundedFactor = Number(factor.toFixed(4))
|
|
307
|
+
const value = currentScaleDistance()
|
|
308
|
+
let state
|
|
309
|
+
try { state = getConversationScalePreview(value) } catch { state = { visible: false, reason: 'unmeasured', mode: 'hidden' } }
|
|
310
|
+
|
|
311
|
+
if (roundedFactor !== calibrationFactor) {
|
|
312
|
+
calibrationFactor = roundedFactor
|
|
313
|
+
canvas.style.transform = `scale(${roundedFactor})`
|
|
314
|
+
viewport.style.height = `${Math.ceil(94 * roundedFactor)}px`
|
|
315
|
+
}
|
|
316
|
+
const scaled = factor < .999
|
|
317
|
+
if (calibration.hasAttribute('data-prts-calibration-scaled') !== scaled) calibration.toggleAttribute('data-prts-calibration-scaled', scaled)
|
|
318
|
+
if (scaleNote && scaleNote.hidden !== !scaled) scaleNote.hidden = !scaled
|
|
319
|
+
const mode = state?.simulated ? 'simulated' : state?.mode || 'hidden'
|
|
320
|
+
const reason = state?.reason || 'unmeasured'
|
|
321
|
+
if (calibration.dataset.prtsCalibrationMode !== mode) calibration.dataset.prtsCalibrationMode = mode
|
|
322
|
+
if (calibration.dataset.prtsCalibrationReason !== reason) calibration.dataset.prtsCalibrationReason = reason
|
|
323
|
+
setText(panel.querySelector('[data-prts-scale-calibration-status]'), calibrationStatus(state, value))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function scheduleCalibrationMeasurement() {
|
|
327
|
+
if (!isOpen) return
|
|
328
|
+
cancelCalibrationFrame()
|
|
329
|
+
calibrationFrame = window?.requestAnimationFrame?.(measureScaleCalibration)
|
|
330
|
+
?? window?.setTimeout?.(measureScaleCalibration, 16)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function scheduleScalePreviewRefresh(entries = []) {
|
|
334
|
+
const viewportEntry = entries.find?.(entry => entry.target?.matches?.('[data-prts-scale-calibration-viewport]'))
|
|
335
|
+
const observedWidth = Number(viewportEntry?.contentRect?.width)
|
|
336
|
+
if (Number.isFinite(observedWidth) && observedWidth > 0) {
|
|
337
|
+
calibrationViewportWidth = observedWidth
|
|
338
|
+
}
|
|
339
|
+
if (!isOpen) return
|
|
340
|
+
setCalibrationResizeState(true)
|
|
341
|
+
if (calibrationSettleTimer !== undefined) window?.clearTimeout?.(calibrationSettleTimer)
|
|
342
|
+
calibrationSettleTimer = window?.setTimeout?.(() => {
|
|
343
|
+
calibrationSettleTimer = undefined
|
|
344
|
+
setCalibrationResizeState(false)
|
|
345
|
+
renderScaleCalibration()
|
|
346
|
+
}, 160)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function renderScaleCalibration() {
|
|
350
|
+
if (!isOpen) return
|
|
351
|
+
if (!panel || !currentPreferences) return
|
|
352
|
+
const value = currentScaleDistance()
|
|
353
|
+
const contrast = currentScaleFocusContrast()
|
|
354
|
+
const lengths = resolveConversationScaleLengths(contrast)
|
|
355
|
+
const calibration = panel.querySelector('[data-prts-scale-calibration]')
|
|
356
|
+
const distanceRange = panel.querySelector('input[data-prts-setting-range][data-prts-setting-key="conversationScaleMaxDistance"]')
|
|
357
|
+
const contrastRange = panel.querySelector('input[data-prts-setting-range][data-prts-setting-key="conversationScaleFocusContrast"]')
|
|
358
|
+
const distanceOutput = panel.querySelector('[data-prts-scale-distance-output]')
|
|
359
|
+
const contrastOutput = panel.querySelector('[data-prts-scale-focus-output]')
|
|
360
|
+
const calibrationValue = panel.querySelector('[data-prts-scale-calibration-value]')
|
|
361
|
+
if (distanceRange && distanceRange.value !== String(value)) distanceRange.value = String(value)
|
|
362
|
+
if (contrastRange && contrastRange.value !== String(contrast)) contrastRange.value = String(contrast)
|
|
363
|
+
setText(distanceOutput, `${value} px`)
|
|
364
|
+
setText(contrastOutput, String(contrast))
|
|
365
|
+
setText(calibrationValue, `${value} px`)
|
|
366
|
+
for (const tick of panel.querySelectorAll('[data-prts-scale-focus-preview-tick]')) {
|
|
367
|
+
const length = lengths[Number(tick.dataset.prtsScaleFocusPreviewTick)] ?? lengths.at(-1)
|
|
368
|
+
const width = `${length}px`
|
|
369
|
+
if (tick.style.width !== width) tick.style.width = width
|
|
370
|
+
}
|
|
371
|
+
const detail = resolveParticleDetail(currentPreferences)
|
|
372
|
+
const detailLabels = { compact: '精简', standard: '标准', precise: '精细' }
|
|
373
|
+
const advanced = panel.querySelector('[data-prts-advanced-summary]')
|
|
374
|
+
setText(advanced, `${detailLabels[detail]} / 区分度 ${contrast} / ${value} px`)
|
|
375
|
+
if (!calibration) return
|
|
376
|
+
const distance = `${value}px`
|
|
377
|
+
if (calibration.style.getPropertyValue('--prts-calibration-distance') !== distance) {
|
|
378
|
+
calibration.style.setProperty('--prts-calibration-distance', distance)
|
|
379
|
+
}
|
|
380
|
+
scheduleCalibrationMeasurement()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function commitScaleDistance() {
|
|
384
|
+
if (!scaleDistanceDirty || !currentPreferences) return false
|
|
385
|
+
const value = currentScaleDistance()
|
|
386
|
+
scaleDistanceDirty = false
|
|
387
|
+
scaleDistanceDraft = value
|
|
388
|
+
if (value !== normalizeScaleDistance(currentPreferences.conversationScaleMaxDistance)) onPreferenceChange('conversationScaleMaxDistance', value)
|
|
389
|
+
else renderScaleCalibration()
|
|
390
|
+
return true
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function commitScaleFocusContrast() {
|
|
394
|
+
if (!scaleFocusContrastDirty || !currentPreferences) return false
|
|
395
|
+
const value = currentScaleFocusContrast()
|
|
396
|
+
scaleFocusContrastDirty = false
|
|
397
|
+
scaleFocusContrastDraft = value
|
|
398
|
+
if (value !== normalizeScaleFocusContrast(currentPreferences.conversationScaleFocusContrast)) onPreferenceChange('conversationScaleFocusContrast', value)
|
|
399
|
+
else renderScaleCalibration()
|
|
400
|
+
return true
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function renderPersistence() {
|
|
404
|
+
const status = panel?.querySelector?.('[data-prts-persistence-status]')
|
|
405
|
+
const error = panel?.querySelector?.('[data-prts-persistence-error]')
|
|
406
|
+
if (!status || !error) return
|
|
407
|
+
const phase = currentPersistence?.phase || 'idle'
|
|
408
|
+
const revision = Number(currentPersistence?.revision) || 0
|
|
409
|
+
const failed = phase === 'error'
|
|
410
|
+
error.hidden = !failed
|
|
411
|
+
if (phase === 'saved' && revision === renderedPersistenceRevision) return
|
|
412
|
+
if (savedTimer !== undefined) window?.clearTimeout?.(savedTimer)
|
|
413
|
+
savedTimer = undefined
|
|
414
|
+
status.hidden = phase !== 'saved'
|
|
415
|
+
status.textContent = phase === 'saved' ? '已保存' : ''
|
|
416
|
+
if (phase === 'saved') {
|
|
417
|
+
renderedPersistenceRevision = revision
|
|
418
|
+
savedTimer = window?.setTimeout?.(() => { status.hidden = true; savedTimer = undefined }, reducedMotion() ? 500 : 1200)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function renderState() {
|
|
423
|
+
if (!panel || !currentPreferences) return
|
|
424
|
+
const particleDetail = resolveParticleDetail(currentPreferences)
|
|
425
|
+
for (const button of panel.querySelectorAll('button[data-prts-setting-key]')) {
|
|
426
|
+
const key = button.dataset.prtsSettingKey
|
|
427
|
+
const actual = key === 'particleDetail' ? particleDetail : currentPreferences[key]
|
|
428
|
+
const selected = String(actual) === button.dataset.prtsSettingValue
|
|
429
|
+
button.classList.toggle('is-selected', selected)
|
|
430
|
+
button.setAttribute('aria-pressed', String(selected))
|
|
431
|
+
if (button.getAttribute('role') === 'radio') button.setAttribute('aria-checked', String(selected))
|
|
432
|
+
}
|
|
433
|
+
const presetLabels = { 'standard-tactical': '标准战术', 'clear-glass': '清晰玻璃', 'quiet-reading': '静谧阅读', custom: '自定义' }
|
|
434
|
+
const preset = presetLabels[currentPreferences.preset] || '自定义'
|
|
435
|
+
const presetStatus = panel.querySelector('[data-prts-settings-preset-status]')
|
|
436
|
+
if (presetStatus) presetStatus.textContent = preset
|
|
437
|
+
const motionStatus = panel.querySelector('[data-prts-effective-motion]')
|
|
438
|
+
if (motionStatus) motionStatus.hidden = !(currentPreferences.motion === 'system' && motionMedia?.matches)
|
|
439
|
+
const transparency = panel.querySelector('[data-prts-transparency-status]')
|
|
440
|
+
if (transparency) transparency.hidden = !(currentPreferences.glass !== 'off' && transparencyMedia?.matches)
|
|
441
|
+
const particleError = panel.querySelector('[data-prts-particle-error]')
|
|
442
|
+
if (particleError) particleError.hidden = currentStatus?.particle?.phase !== 'error'
|
|
443
|
+
if (!scaleDistanceDirty) scaleDistanceDraft = normalizeScaleDistance(currentPreferences.conversationScaleMaxDistance)
|
|
444
|
+
if (!scaleFocusContrastDirty) scaleFocusContrastDraft = normalizeScaleFocusContrast(currentPreferences.conversationScaleFocusContrast)
|
|
445
|
+
renderScaleCalibration()
|
|
446
|
+
renderPersistence()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function markFeedback(target) {
|
|
450
|
+
if (!target) return
|
|
451
|
+
if (feedbackTimer !== undefined) window?.clearTimeout?.(feedbackTimer)
|
|
452
|
+
feedbackTarget?.removeAttribute?.('data-prts-setting-feedback')
|
|
453
|
+
feedbackTarget = target
|
|
454
|
+
target.setAttribute('data-prts-setting-feedback', '')
|
|
455
|
+
feedbackTimer = window?.setTimeout?.(() => {
|
|
456
|
+
target.removeAttribute('data-prts-setting-feedback')
|
|
457
|
+
if (feedbackTarget === target) feedbackTarget = undefined
|
|
458
|
+
feedbackTimer = undefined
|
|
459
|
+
}, 300)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function onPanelClick(event) {
|
|
463
|
+
if (event.target === panel) { setOpen(false); return }
|
|
464
|
+
const target = event.target?.closest?.('button')
|
|
465
|
+
if (!target) return
|
|
466
|
+
if (target.hasAttribute('data-prts-theme-settings-close')) { setOpen(false); return }
|
|
467
|
+
if (target.hasAttribute('data-prts-setting-key')) {
|
|
468
|
+
const raw = target.dataset.prtsSettingValue
|
|
469
|
+
markFeedback(target)
|
|
470
|
+
onPreferenceChange(target.dataset.prtsSettingKey, raw === 'true' ? true : raw === 'false' ? false : raw)
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
if (target.hasAttribute('data-prts-retry-save')) { onRetrySave(); return }
|
|
474
|
+
if (target.hasAttribute('data-prts-reset-visual')) { setResetConfirmation(true); return }
|
|
475
|
+
if (target.hasAttribute('data-prts-reset-cancel')) { setResetConfirmation(false); return }
|
|
476
|
+
if (target.hasAttribute('data-prts-reset-confirm-action')) {
|
|
477
|
+
scaleDistanceDirty = false
|
|
478
|
+
scaleDistanceDraft = undefined
|
|
479
|
+
scaleFocusContrastDirty = false
|
|
480
|
+
scaleFocusContrastDraft = undefined
|
|
481
|
+
setResetConfirmation(false)
|
|
482
|
+
onResetVisual()
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function onPanelInput(event) {
|
|
487
|
+
const target = event.target
|
|
488
|
+
if (!target?.matches?.('input[data-prts-setting-range]')) return
|
|
489
|
+
const value = Number(target.value)
|
|
490
|
+
if (!Number.isFinite(value)) return
|
|
491
|
+
if (target.dataset.prtsSettingKey === 'conversationScaleMaxDistance') {
|
|
492
|
+
scaleDistanceDraft = value
|
|
493
|
+
scaleDistanceDirty = true
|
|
494
|
+
} else if (target.dataset.prtsSettingKey === 'conversationScaleFocusContrast') {
|
|
495
|
+
scaleFocusContrastDraft = value
|
|
496
|
+
scaleFocusContrastDirty = true
|
|
497
|
+
} else {
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
renderScaleCalibration()
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function onKeydown(event) {
|
|
504
|
+
if (!isOpen) return
|
|
505
|
+
if (event.key === 'Escape') { event.preventDefault(); setOpen(false); return }
|
|
506
|
+
if (nativeModal || event.key !== 'Tab') return
|
|
507
|
+
const nodes = focusable()
|
|
508
|
+
if (!nodes.length) return
|
|
509
|
+
const first = nodes[0]
|
|
510
|
+
const last = nodes.at(-1)
|
|
511
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() }
|
|
512
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function onCancel(event) { event.preventDefault(); setOpen(false) }
|
|
516
|
+
const toggle = () => setOpen(!isOpen)
|
|
517
|
+
const closeFromBackdrop = () => setOpen(false)
|
|
518
|
+
const renderMediaState = () => renderState()
|
|
519
|
+
const commitFromControl = event => {
|
|
520
|
+
const target = event.target
|
|
521
|
+
if (!target?.matches?.('input[data-prts-setting-range]')) return
|
|
522
|
+
if (target.dataset.prtsSettingKey === 'conversationScaleMaxDistance') commitScaleDistance()
|
|
523
|
+
if (target.dataset.prtsSettingKey === 'conversationScaleFocusContrast') commitScaleFocusContrast()
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
trigger?.addEventListener('click', toggle)
|
|
527
|
+
panel?.addEventListener('click', onPanelClick)
|
|
528
|
+
panel?.addEventListener('cancel', onCancel)
|
|
529
|
+
panel?.addEventListener('input', onPanelInput)
|
|
530
|
+
panel?.addEventListener('change', commitFromControl)
|
|
531
|
+
panel?.addEventListener('pointerup', commitFromControl)
|
|
532
|
+
panel?.addEventListener('focusout', commitFromControl)
|
|
533
|
+
backdrop?.addEventListener('click', closeFromBackdrop)
|
|
534
|
+
document.addEventListener('keydown', onKeydown)
|
|
535
|
+
transparencyMedia?.addEventListener?.('change', renderMediaState)
|
|
536
|
+
motionMedia?.addEventListener?.('change', renderMediaState)
|
|
537
|
+
if (typeof window?.ResizeObserver === 'function') {
|
|
538
|
+
calibrationResizeObserver = new window.ResizeObserver(scheduleScalePreviewRefresh)
|
|
539
|
+
const viewport = panel?.querySelector?.('[data-prts-scale-calibration-viewport]')
|
|
540
|
+
if (viewport) calibrationResizeObserver.observe(viewport)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
update(preferences, status, persistenceState = currentPersistence) {
|
|
545
|
+
currentPreferences = preferences
|
|
546
|
+
currentStatus = status
|
|
547
|
+
currentPersistence = persistenceState ?? { phase: 'idle', revision: 0 }
|
|
548
|
+
if (!scaleDistanceDirty) scaleDistanceDraft = normalizeScaleDistance(preferences?.conversationScaleMaxDistance)
|
|
549
|
+
if (!scaleFocusContrastDirty) scaleFocusContrastDraft = normalizeScaleFocusContrast(preferences?.conversationScaleFocusContrast)
|
|
550
|
+
renderState()
|
|
551
|
+
},
|
|
552
|
+
refreshScalePreview: renderScaleCalibration,
|
|
553
|
+
scheduleScalePreviewRefresh,
|
|
554
|
+
open: () => setOpen(true),
|
|
555
|
+
close: () => setOpen(false),
|
|
556
|
+
toggle,
|
|
557
|
+
dispose() {
|
|
558
|
+
isOpen = false
|
|
559
|
+
hidePanel()
|
|
560
|
+
if (savedTimer !== undefined) window?.clearTimeout?.(savedTimer)
|
|
561
|
+
if (feedbackTimer !== undefined) window?.clearTimeout?.(feedbackTimer)
|
|
562
|
+
if (calibrationSettleTimer !== undefined) window?.clearTimeout?.(calibrationSettleTimer)
|
|
563
|
+
calibrationSettleTimer = undefined
|
|
564
|
+
cancelCalibrationFrame()
|
|
565
|
+
setCalibrationResizeState(false)
|
|
566
|
+
feedbackTarget?.removeAttribute?.('data-prts-setting-feedback')
|
|
567
|
+
trigger?.setAttribute('aria-expanded', 'false')
|
|
568
|
+
trigger?.removeEventListener('click', toggle)
|
|
569
|
+
panel?.removeEventListener('click', onPanelClick)
|
|
570
|
+
panel?.removeEventListener('cancel', onCancel)
|
|
571
|
+
panel?.removeEventListener('input', onPanelInput)
|
|
572
|
+
panel?.removeEventListener('change', commitFromControl)
|
|
573
|
+
panel?.removeEventListener('pointerup', commitFromControl)
|
|
574
|
+
panel?.removeEventListener('focusout', commitFromControl)
|
|
575
|
+
backdrop?.removeEventListener('click', closeFromBackdrop)
|
|
576
|
+
document.removeEventListener('keydown', onKeydown)
|
|
577
|
+
transparencyMedia?.removeEventListener?.('change', renderMediaState)
|
|
578
|
+
motionMedia?.removeEventListener?.('change', renderMediaState)
|
|
579
|
+
calibrationResizeObserver?.disconnect()
|
|
580
|
+
document.documentElement.removeAttribute('data-prts-settings-open')
|
|
581
|
+
},
|
|
582
|
+
}
|
|
583
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const TO_BOTTOM_ATTRIBUTE = 'data-prts-to-bottom'
|
|
2
|
+
|
|
3
|
+
function isToBottomButton(node) {
|
|
4
|
+
return node?.tagName === 'BUTTON'
|
|
5
|
+
&& [...(node.classList ?? [])].some(token => token.endsWith('_toBottom'))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createToBottomAdapter({ document, window }) {
|
|
9
|
+
let observer
|
|
10
|
+
let operation
|
|
11
|
+
const owned = new Set()
|
|
12
|
+
const pending = new Set()
|
|
13
|
+
|
|
14
|
+
function collectButtons(node) {
|
|
15
|
+
if (node?.nodeType !== 1) return
|
|
16
|
+
if (node.tagName === 'BUTTON') pending.add(node)
|
|
17
|
+
for (const button of node.querySelectorAll?.('button') ?? []) pending.add(button)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function process(button) {
|
|
21
|
+
if (button?.isConnected && operation?.contains(button) && isToBottomButton(button)) {
|
|
22
|
+
button.setAttribute(TO_BOTTOM_ATTRIBUTE, '')
|
|
23
|
+
owned.add(button)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
button?.removeAttribute?.(TO_BOTTOM_ATTRIBUTE)
|
|
27
|
+
owned.delete(button)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function scan() {
|
|
31
|
+
for (const button of pending) process(button)
|
|
32
|
+
pending.clear()
|
|
33
|
+
for (const button of [...owned]) {
|
|
34
|
+
if (!button.isConnected) process(button)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function queueScan() {
|
|
39
|
+
scan()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function onMutations(mutations) {
|
|
43
|
+
for (const mutation of mutations) {
|
|
44
|
+
if (mutation.type === 'attributes') collectButtons(mutation.target)
|
|
45
|
+
for (const node of mutation.addedNodes ?? []) collectButtons(node)
|
|
46
|
+
for (const node of mutation.removedNodes ?? []) {
|
|
47
|
+
if (node?.nodeType !== 1) continue
|
|
48
|
+
for (const button of owned) {
|
|
49
|
+
if (button === node || node.contains?.(button)) pending.add(button)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (pending.size) queueScan()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clear() {
|
|
57
|
+
for (const button of owned) button.removeAttribute?.(TO_BOTTOM_ATTRIBUTE)
|
|
58
|
+
owned.clear()
|
|
59
|
+
pending.clear()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
start() {
|
|
64
|
+
if (!document) return
|
|
65
|
+
operation = document.querySelector('[data-prts-region="operation"]')
|
|
66
|
+
if (!operation) return
|
|
67
|
+
collectButtons(operation)
|
|
68
|
+
scan()
|
|
69
|
+
if (observer || !window?.MutationObserver) return
|
|
70
|
+
observer = new window.MutationObserver(onMutations)
|
|
71
|
+
observer.observe(operation, {
|
|
72
|
+
subtree: true,
|
|
73
|
+
childList: true,
|
|
74
|
+
attributes: true,
|
|
75
|
+
attributeFilter: ['class'],
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
dispose() {
|
|
79
|
+
observer?.disconnect()
|
|
80
|
+
observer = undefined
|
|
81
|
+
clear()
|
|
82
|
+
operation = undefined
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|