@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.
Files changed (50) hide show
  1. package/LICENSE +24 -0
  2. package/README.en.md +103 -0
  3. package/README.md +107 -0
  4. package/THIRD_PARTY_NOTICES.md +48 -0
  5. package/cordis.patch.yml +4 -0
  6. package/lib/client.js +7466 -0
  7. package/lib/index.js +3 -0
  8. package/package.json +64 -0
  9. package/src/assets/faction-emblems/lungmen.png +0 -0
  10. package/src/assets/faction-emblems/penguin-logistics.png +0 -0
  11. package/src/assets/faction-emblems/reunion.png +0 -0
  12. package/src/assets/faction-emblems/rhine-lab.png +0 -0
  13. package/src/assets/faction-emblems/rhodes-island.png +0 -0
  14. package/src/assets/prts-original-avatar.png +0 -0
  15. package/src/assets/rhodes-dsh.svg +6 -0
  16. package/src/assets/topographic-map.svg +72 -0
  17. package/src/assets/topographic-tech.svg +19 -0
  18. package/src/client/assistant-glass-adapter.js +231 -0
  19. package/src/client/composer-glass-adapter.js +210 -0
  20. package/src/client/conversation-control-adapter.js +109 -0
  21. package/src/client/conversation-scale-adapter.js +2243 -0
  22. package/src/client/facility-geometry.js +60 -0
  23. package/src/client/facility-vector.js +230 -0
  24. package/src/client/floating-glass-adapter.js +177 -0
  25. package/src/client/host-geometry-adapter.js +123 -0
  26. package/src/client/index.js +359 -0
  27. package/src/client/layout-stability-diagnostics.js +162 -0
  28. package/src/client/operations-shell-v2.js +248 -0
  29. package/src/client/particle-field-adapter.js +1840 -0
  30. package/src/client/preferences.js +239 -0
  31. package/src/client/rc7-adapter.js +83 -0
  32. package/src/client/resize-shield-adapter.js +64 -0
  33. package/src/client/settings-page.js +62 -0
  34. package/src/client/sidebar-control-adapter.js +317 -0
  35. package/src/client/startup-sequence.js +366 -0
  36. package/src/client/status-projection.js +79 -0
  37. package/src/client/terrain-motion-adapter.js +188 -0
  38. package/src/client/theme-controller-v2.js +134 -0
  39. package/src/client/theme-controller.js +119 -0
  40. package/src/client/theme-settings-workbench.js +583 -0
  41. package/src/client/to-bottom-adapter.js +85 -0
  42. package/src/client/ui-store.js +23 -0
  43. package/src/index.js +4 -0
  44. package/src/styles/conversation-scale.css +471 -0
  45. package/src/styles/facility-vector.css +292 -0
  46. package/src/styles/prts.css +1223 -0
  47. package/src/styles/refinement.css +429 -0
  48. package/src/styles/stabilization.css +161 -0
  49. package/src/styles/startup-sequence.css +496 -0
  50. package/src/styles/theme-settings-workbench.css +803 -0
@@ -0,0 +1,239 @@
1
+ export const PRTS_STORAGE_KEY = 'dsh.ui.prts.v1'
2
+
3
+ export const CONVERSATION_SCALE_DISTANCE_MIN = 16
4
+ export const CONVERSATION_SCALE_DISTANCE_MAX = 240
5
+ export const CONVERSATION_SCALE_DISTANCE_STEP = 8
6
+ export const CONVERSATION_SCALE_DISTANCE_DEFAULT = 96
7
+
8
+ export const CONVERSATION_SCALE_FOCUS_CONTRAST_MIN = 0
9
+ export const CONVERSATION_SCALE_FOCUS_CONTRAST_MAX = 100
10
+ export const CONVERSATION_SCALE_FOCUS_CONTRAST_STEP = 10
11
+ export const CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT = 70
12
+
13
+ export const DEFAULT_PREFERENCES = Object.freeze({
14
+ version: 6,
15
+ enabled: false,
16
+ preset: 'standard-tactical',
17
+ texture: 'full',
18
+ glass: 'standard',
19
+ motion: 'system',
20
+ bootAnimation: true,
21
+ conversationParticleDensity: 'sparse',
22
+ heroParticleDensity: 'light',
23
+ particlePattern: 'orthogonal',
24
+ conversationScaleMaxDistance: CONVERSATION_SCALE_DISTANCE_DEFAULT,
25
+ conversationScaleFocusContrast: CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT,
26
+ })
27
+
28
+ export const VISUAL_PRESETS = Object.freeze({
29
+ 'standard-tactical': Object.freeze({
30
+ texture: 'full',
31
+ glass: 'standard',
32
+ motion: 'system',
33
+ conversationParticleDensity: 'sparse',
34
+ heroParticleDensity: 'light',
35
+ particlePattern: 'orthogonal',
36
+ }),
37
+ 'clear-glass': Object.freeze({
38
+ texture: 'restrained',
39
+ glass: 'clear',
40
+ motion: 'system',
41
+ conversationParticleDensity: 'sparse',
42
+ heroParticleDensity: 'light',
43
+ particlePattern: 'orthogonal',
44
+ }),
45
+ 'quiet-reading': Object.freeze({
46
+ texture: 'restrained',
47
+ glass: 'soft',
48
+ motion: 'reduced',
49
+ conversationParticleDensity: 'sparse',
50
+ heroParticleDensity: 'light',
51
+ particlePattern: 'orthogonal',
52
+ }),
53
+ })
54
+
55
+
56
+ export const PARTICLE_DETAIL_LEVELS = Object.freeze({
57
+ compact: Object.freeze({ conversationParticleDensity: 'sparse', heroParticleDensity: 'light' }),
58
+ standard: Object.freeze({ conversationParticleDensity: 'light', heroParticleDensity: 'standard' }),
59
+ precise: Object.freeze({ conversationParticleDensity: 'dense', heroParticleDensity: 'ultra' }),
60
+ })
61
+ export const PREFERENCE_GROUPS = Object.freeze({
62
+ background: Object.freeze(['texture']),
63
+ particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity']),
64
+ material: Object.freeze(['glass']),
65
+ accessibility: Object.freeze(['motion', 'bootAnimation']),
66
+ navigation: Object.freeze(['conversationScaleMaxDistance', 'conversationScaleFocusContrast']),
67
+ })
68
+
69
+ const TEXTURES = new Set(['off', 'restrained', 'full'])
70
+ const GLASS_STRENGTHS = new Set(['off', 'soft', 'standard', 'clear'])
71
+ const MOTIONS = new Set(['system', 'reduced'])
72
+ const PARTICLE_DENSITIES = new Set(['sparse', 'light', 'standard', 'dense', 'ultra'])
73
+ const PRESETS = new Set([...Object.keys(VISUAL_PRESETS), 'custom'])
74
+ const PRESET_LINKED_KEYS = new Set(Object.keys(VISUAL_PRESETS['standard-tactical']))
75
+
76
+
77
+ function matchedVisualPreset(value) {
78
+ for (const [preset, settings] of Object.entries(VISUAL_PRESETS)) {
79
+ if ([...PRESET_LINKED_KEYS].every(key => value[key] === settings[key])) return preset
80
+ }
81
+ return 'custom'
82
+ }
83
+
84
+ export function resolveParticleDetail(value) {
85
+ const normalized = normalizePreferences(value)
86
+ const exact = Object.entries(PARTICLE_DETAIL_LEVELS).find(([, settings]) => (
87
+ normalized.conversationParticleDensity === settings.conversationParticleDensity
88
+ && normalized.heroParticleDensity === settings.heroParticleDensity
89
+ ))
90
+ if (exact) return exact[0]
91
+ const rank = { sparse: 0, light: 1, standard: 2, dense: 3, ultra: 4 }
92
+ const score = ((rank[normalized.conversationParticleDensity] ?? 0) + (rank[normalized.heroParticleDensity] ?? 1)) / 2
93
+ return score < 1.5 ? 'compact' : score < 3 ? 'standard' : 'precise'
94
+ }
95
+ function booleanOr(value, fallback) {
96
+ return typeof value === 'boolean' ? value : fallback
97
+ }
98
+
99
+ function enumOr(value, allowed, fallback) {
100
+ return allowed.has(value) ? value : fallback
101
+ }
102
+
103
+ function scaleDistanceOr(value, fallback) {
104
+ const numeric = Number(value)
105
+ if (!Number.isFinite(numeric)) return fallback
106
+ const clamped = Math.min(CONVERSATION_SCALE_DISTANCE_MAX, Math.max(CONVERSATION_SCALE_DISTANCE_MIN, numeric))
107
+ return Math.round(clamped / CONVERSATION_SCALE_DISTANCE_STEP) * CONVERSATION_SCALE_DISTANCE_STEP
108
+ }
109
+
110
+ function scaleFocusContrastOr(value, fallback) {
111
+ const numeric = Number(value)
112
+ if (!Number.isFinite(numeric)) return fallback
113
+ const clamped = Math.min(CONVERSATION_SCALE_FOCUS_CONTRAST_MAX, Math.max(CONVERSATION_SCALE_FOCUS_CONTRAST_MIN, numeric))
114
+ return Math.round(clamped / CONVERSATION_SCALE_FOCUS_CONTRAST_STEP) * CONVERSATION_SCALE_FOCUS_CONTRAST_STEP
115
+ }
116
+
117
+ function normalizeGlass(input) {
118
+ if (input.glassEnabled === false || input.glass === 'off') return 'off'
119
+ if (input.glass === 'liquid') return 'clear'
120
+ return enumOr(input.glass, GLASS_STRENGTHS, DEFAULT_PREFERENCES.glass)
121
+ }
122
+
123
+ function legacyParticleDensities(input) {
124
+ const source = input.particleDensities !== null
125
+ && typeof input.particleDensities === 'object'
126
+ && !Array.isArray(input.particleDensities)
127
+ ? input.particleDensities
128
+ : null
129
+ const selected = source?.[input.particlePattern]
130
+ return {
131
+ conversation: input.conversationParticleDensity ?? selected?.conversation,
132
+ hero: input.heroParticleDensity ?? selected?.hero,
133
+ }
134
+ }
135
+
136
+ export function normalizePreferences(value) {
137
+ const input = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
138
+ const migratedPreset = input.version === 1 && !Object.hasOwn(input, 'preset')
139
+ ? 'custom'
140
+ : input.preset
141
+ const particlePreset = VISUAL_PRESETS[input.preset] ?? DEFAULT_PREFERENCES
142
+ const legacyDensities = legacyParticleDensities(input)
143
+ const normalized = {
144
+ version: 6,
145
+ enabled: booleanOr(input.enabled, DEFAULT_PREFERENCES.enabled),
146
+ preset: enumOr(migratedPreset, PRESETS, DEFAULT_PREFERENCES.preset),
147
+ texture: enumOr(input.texture, TEXTURES, DEFAULT_PREFERENCES.texture),
148
+ glass: normalizeGlass(input),
149
+ motion: input.motion === 'full' ? 'system' : enumOr(input.motion, MOTIONS, DEFAULT_PREFERENCES.motion),
150
+ bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
151
+ conversationParticleDensity: enumOr(legacyDensities.conversation, PARTICLE_DENSITIES, particlePreset.conversationParticleDensity),
152
+ heroParticleDensity: enumOr(legacyDensities.hero, PARTICLE_DENSITIES, particlePreset.heroParticleDensity),
153
+ particlePattern: 'orthogonal',
154
+ conversationScaleMaxDistance: scaleDistanceOr(
155
+ input.conversationScaleMaxDistance,
156
+ DEFAULT_PREFERENCES.conversationScaleMaxDistance,
157
+ ),
158
+ conversationScaleFocusContrast: scaleFocusContrastOr(
159
+ input.conversationScaleFocusContrast,
160
+ DEFAULT_PREFERENCES.conversationScaleFocusContrast,
161
+ ),
162
+ }
163
+ normalized.preset = matchedVisualPreset(normalized)
164
+ return normalized
165
+ }
166
+
167
+ export function applyVisualPreset(value, preset) {
168
+ const current = normalizePreferences(value)
169
+ const settings = VISUAL_PRESETS[preset]
170
+ if (!settings) return { ...current, preset: 'custom' }
171
+ return normalizePreferences({
172
+ ...current,
173
+ ...settings,
174
+ })
175
+ }
176
+
177
+
178
+ export function applyParticleDetail(value, level) {
179
+ const current = normalizePreferences(value)
180
+ const settings = PARTICLE_DETAIL_LEVELS[level]
181
+ return settings ? normalizePreferences({ ...current, ...settings }) : current
182
+ }
183
+ export function updatePreferenceValue(value, key, nextValue) {
184
+ if (key === 'preset') return applyVisualPreset(value, nextValue)
185
+ if (key === 'particleDetail') return applyParticleDetail(value, nextValue)
186
+ const current = normalizePreferences(value)
187
+ if (key === 'conversationParticleDensity' || key === 'heroParticleDensity') {
188
+ const density = enumOr(nextValue, PARTICLE_DENSITIES, current[key])
189
+ return normalizePreferences({ ...current, [key]: density })
190
+ }
191
+ return normalizePreferences({ ...current, [key]: nextValue })
192
+ }
193
+
194
+ export function resetPreferenceGroup(value, group) {
195
+ const current = normalizePreferences(value)
196
+ const keys = PREFERENCE_GROUPS[group]
197
+ if (!keys) return current
198
+ const reset = { ...current }
199
+ for (const key of keys) reset[key] = DEFAULT_PREFERENCES[key]
200
+ return normalizePreferences(reset)
201
+ }
202
+
203
+ export function loadPreferences(storage) {
204
+ try {
205
+ const raw = storage?.getItem(PRTS_STORAGE_KEY)
206
+ if (raw === null || raw === undefined) return { ...DEFAULT_PREFERENCES }
207
+ const parsed = JSON.parse(raw)
208
+ if (![1, 2, 3, 4, 5, 6].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
209
+ return normalizePreferences(parsed)
210
+ } catch {
211
+ return { ...DEFAULT_PREFERENCES }
212
+ }
213
+ }
214
+
215
+ export function persistPreferences(storage, value) {
216
+ const normalized = normalizePreferences(value)
217
+ let persisted = false
218
+ try {
219
+ if (typeof storage?.setItem === 'function') {
220
+ storage.setItem(PRTS_STORAGE_KEY, JSON.stringify(normalized))
221
+ persisted = true
222
+ }
223
+ } catch {
224
+ // Private browsing or storage policy can make persistence unavailable.
225
+ }
226
+ return { preferences: normalized, persisted }
227
+ }
228
+
229
+ export function savePreferences(storage, value) {
230
+ return persistPreferences(storage, value).preferences
231
+ }
232
+
233
+ export function isSafeMode(search = '') {
234
+ try {
235
+ return new URLSearchParams(search).get('prts-safe') === '1'
236
+ } catch {
237
+ return false
238
+ }
239
+ }
@@ -0,0 +1,83 @@
1
+ function closestCommonContainer(root, nodes) {
2
+ let candidate = nodes[0]?.parentElement
3
+ while (candidate && candidate !== root) {
4
+ if (nodes.every(node => candidate === node || candidate.contains(node))) return candidate
5
+ candidate = candidate.parentElement
6
+ }
7
+ return root && nodes.every(node => root.contains(node)) ? root : null
8
+ }
9
+
10
+ export function resolveRc7Regions(document, report = () => {}) {
11
+ const root = document?.querySelector('[data-slot="root"]')
12
+ const sidebarSlot = root?.querySelector('[data-slot="sidebar"]')
13
+ const conversationSlot = root?.querySelector('[data-slot="conversation"]')
14
+ const detailsSlot = root?.querySelector('[data-slot="details"]')
15
+ if (!root || !sidebarSlot || !conversationSlot || !detailsSlot) {
16
+ report('required layout slots are unavailable', {
17
+ root: Boolean(root), sidebar: Boolean(sidebarSlot),
18
+ conversation: Boolean(conversationSlot), details: Boolean(detailsSlot),
19
+ })
20
+ return null
21
+ }
22
+ const sidebar = sidebarSlot?.parentElement
23
+ const center = conversationSlot?.parentElement
24
+ const details = detailsSlot?.parentElement
25
+
26
+ if (!sidebar || !center || !details) {
27
+ report('required layout region containers are unavailable', {
28
+ sidebar: Boolean(sidebar), conversation: Boolean(center), details: Boolean(details),
29
+ })
30
+ return null
31
+ }
32
+
33
+ const frame = closestCommonContainer(root, [sidebar, center, details])
34
+ if (!frame) {
35
+ report('common layout container is unavailable')
36
+ return null
37
+ }
38
+
39
+ return { root, frame, sidebar, center, details, sidebarSlot, conversationSlot, detailsSlot }
40
+ }
41
+
42
+ export function createRc7Adapter({ document, warn } = {}) {
43
+ const owned = []
44
+ let mounted
45
+ let lastDiagnostic
46
+ const writeWarning = typeof warn === 'function'
47
+ ? warn
48
+ : (...args) => document?.defaultView?.console?.warn?.(...args)
49
+
50
+ function reportLayoutIssue(message, details) {
51
+ const signature = `${message}:${JSON.stringify(details ?? {})}`
52
+ if (signature === lastDiagnostic) return
53
+ lastDiagnostic = signature
54
+ writeWarning(`[dsh-theme-prts] ${message}`, details)
55
+ }
56
+
57
+ return {
58
+ mount() {
59
+ if (mounted) return mounted
60
+ const regions = resolveRc7Regions(document, reportLayoutIssue)
61
+ if (!regions) return null
62
+ lastDiagnostic = undefined
63
+ for (const [node, value] of [
64
+ [regions.frame, 'frame'],
65
+ [regions.sidebar, 'sessions'],
66
+ [regions.center, 'operation'],
67
+ [regions.details, 'details'],
68
+ ]) {
69
+ owned.push([node, node.getAttribute('data-prts-region')])
70
+ node.setAttribute('data-prts-region', value)
71
+ }
72
+ mounted = regions
73
+ return mounted
74
+ },
75
+ dispose() {
76
+ for (const [node, previous] of owned.splice(0).reverse()) {
77
+ if (previous === null) node.removeAttribute('data-prts-region')
78
+ else node.setAttribute('data-prts-region', previous)
79
+ }
80
+ mounted = undefined
81
+ },
82
+ }
83
+ }
@@ -0,0 +1,64 @@
1
+ const RESIZE_SHIELD_ATTRIBUTE = 'data-prts-resize-shield'
2
+ const DEFAULT_SETTLE_DELAY = 180
3
+
4
+ export function createResizeShieldAdapter({
5
+ window,
6
+ settleDelay = DEFAULT_SETTLE_DELAY,
7
+ } = {}) {
8
+ let targets = []
9
+ let settleTimer
10
+ let started = false
11
+
12
+ function setActive(active) {
13
+ for (const node of targets) {
14
+ if (!node || node.hasAttribute?.(RESIZE_SHIELD_ATTRIBUTE) === active) continue
15
+ node.toggleAttribute?.(RESIZE_SHIELD_ATTRIBUTE, active)
16
+ }
17
+ }
18
+
19
+ function clearTimer() {
20
+ if (settleTimer === undefined) return
21
+ window?.clearTimeout?.(settleTimer)
22
+ settleTimer = undefined
23
+ }
24
+
25
+ function settle() {
26
+ settleTimer = undefined
27
+ setActive(false)
28
+ }
29
+
30
+ function onResize() {
31
+ if (!started) return
32
+ setActive(true)
33
+ clearTimer()
34
+ settleTimer = window?.setTimeout?.(settle, settleDelay)
35
+ }
36
+
37
+ function dispose() {
38
+ clearTimer()
39
+ window?.removeEventListener?.('resize', onResize)
40
+ setActive(false)
41
+ targets = []
42
+ started = false
43
+ }
44
+
45
+ return {
46
+ start(nextTargets = []) {
47
+ const resolved = [...new Set(nextTargets)].filter(Boolean)
48
+ if (!window || !resolved.length) {
49
+ dispose()
50
+ return false
51
+ }
52
+ if (started) {
53
+ clearTimer()
54
+ setActive(false)
55
+ window.removeEventListener?.('resize', onResize)
56
+ }
57
+ targets = resolved
58
+ started = true
59
+ window.addEventListener?.('resize', onResize, { passive: true })
60
+ return true
61
+ },
62
+ dispose,
63
+ }
64
+ }
@@ -0,0 +1,62 @@
1
+ export function createSettingsPage(React, meta = {}) {
2
+ const h = React.createElement
3
+
4
+ return function PrtsPluginSettingsPage({ useStore, updatePreference }) {
5
+ const { preferences } = useStore(state => ({ preferences: state.preferences }))
6
+ const safeMode = Boolean(meta.safeMode)
7
+ const enabled = Boolean(preferences.enabled) && !safeMode
8
+ const bootAnimation = preferences.bootAnimation !== false
9
+
10
+ const settingRow = ({ key, title, description, checked, disabled = false, onChange }) => h('div', {
11
+ className: 'prts-plugin-settings__row',
12
+ key,
13
+ 'data-prts-plugin-setting': key,
14
+ },
15
+ h('span', { className: 'prts-plugin-settings__copy' },
16
+ h('strong', null, title),
17
+ h('small', null, description),
18
+ ),
19
+ h('button', {
20
+ type: 'button',
21
+ role: 'switch',
22
+ className: checked ? 'is-active' : '',
23
+ 'aria-label': title,
24
+ 'aria-checked': String(checked),
25
+ disabled,
26
+ onClick: onChange,
27
+ }, h('span', { 'aria-hidden': true })),
28
+ )
29
+
30
+ return h('section', {
31
+ className: 'prts-plugin-settings',
32
+ 'data-prts-plugin-settings': '',
33
+ 'data-prts-settings-enabled': String(enabled),
34
+ 'aria-label': 'P.R.T.S. 插件设置',
35
+ },
36
+ h('header', { className: 'prts-plugin-settings__header' },
37
+ h('h3', null, 'P.R.T.S.'),
38
+ h('p', null, '完整外观设置位于主界面左上角的罗德岛徽记。'),
39
+ ),
40
+ safeMode && h('p', { className: 'prts-plugin-settings__safe', role: 'status' },
41
+ '安全模式已暂停主题。移除地址中的 ?prts-safe=1 后即可恢复。',
42
+ ),
43
+ h('div', { className: 'prts-plugin-settings__list' },
44
+ settingRow({
45
+ key: 'enabled',
46
+ title: '启用 P.R.T.S. 主题',
47
+ description: safeMode ? '安全模式下无法启用主题' : '控制 P.R.T.S. 外观是否加载',
48
+ checked: enabled,
49
+ disabled: safeMode,
50
+ onChange: () => { if (!safeMode) updatePreference('enabled', !preferences.enabled) },
51
+ }),
52
+ settingRow({
53
+ key: 'bootAnimation',
54
+ title: '播放启动动画',
55
+ description: '刷新或启用主题时显示 P.R.T.S. 启动动画',
56
+ checked: bootAnimation,
57
+ onChange: () => updatePreference('bootAnimation', !bootAnimation),
58
+ }),
59
+ ),
60
+ )
61
+ }
62
+ }