@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,79 @@
|
|
|
1
|
+
export const EMPTY_STATUS = Object.freeze({
|
|
2
|
+
connection: 'unknown',
|
|
3
|
+
connectionLabel: '未知',
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
const CONNECTION_LABELS = Object.freeze({
|
|
7
|
+
connected: '已连接',
|
|
8
|
+
connecting: '连接中',
|
|
9
|
+
disconnected: '已断开',
|
|
10
|
+
unknown: '未知',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
function snapshotOf(source) {
|
|
14
|
+
try {
|
|
15
|
+
return source?.getSnapshot?.()
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function subscribeTo(source, listener) {
|
|
22
|
+
try {
|
|
23
|
+
return typeof source?.subscribe === 'function' ? source.subscribe(listener) : () => {}
|
|
24
|
+
} catch {
|
|
25
|
+
return () => {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function connectionState(connection) {
|
|
30
|
+
if (!connection) return 'unknown'
|
|
31
|
+
const stateSource = connection.state ?? connection.status
|
|
32
|
+
const state = snapshotOf(stateSource)
|
|
33
|
+
if (state === 'connected') return 'connected'
|
|
34
|
+
if (state === 'connecting' || state === 'reconnecting') return 'connecting'
|
|
35
|
+
if (state === 'disconnected') return 'disconnected'
|
|
36
|
+
if (connection.hostDescription?.getSnapshot) {
|
|
37
|
+
return snapshotOf(connection.hostDescription) ? 'connected' : 'connecting'
|
|
38
|
+
}
|
|
39
|
+
return 'unknown'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function projectHarnessStatus({ connection } = {}) {
|
|
43
|
+
const connectionValue = connectionState(connection)
|
|
44
|
+
return {
|
|
45
|
+
connection: connectionValue,
|
|
46
|
+
connectionLabel: CONNECTION_LABELS[connectionValue],
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function equalStatus(previous, next) {
|
|
51
|
+
return previous?.connection === next.connection
|
|
52
|
+
&& previous?.connectionLabel === next.connectionLabel
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createHarnessStatusSource({ connection, onChange }) {
|
|
56
|
+
let disposed = false
|
|
57
|
+
let lastStatus
|
|
58
|
+
const stops = []
|
|
59
|
+
|
|
60
|
+
function publish() {
|
|
61
|
+
if (disposed) return
|
|
62
|
+
const nextStatus = projectHarnessStatus({ connection })
|
|
63
|
+
if (equalStatus(lastStatus, nextStatus)) return
|
|
64
|
+
lastStatus = nextStatus
|
|
65
|
+
onChange(nextStatus)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
stops.push(subscribeTo(connection?.hostDescription, publish))
|
|
69
|
+
stops.push(subscribeTo(connection?.state ?? connection?.status, publish))
|
|
70
|
+
publish()
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
refresh: publish,
|
|
74
|
+
dispose() {
|
|
75
|
+
disposed = true
|
|
76
|
+
for (const stop of stops.splice(0).reverse()) stop()
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const PARALLAX_RATIO = 0.2
|
|
2
|
+
const TILE_HEIGHT = 4800
|
|
3
|
+
const DESKTOP_MAX_LAG = 42
|
|
4
|
+
const TABLET_MAX_LAG = 20
|
|
5
|
+
const PHONE_BREAKPOINT = 640
|
|
6
|
+
const TABLET_BREAKPOINT = 1024
|
|
7
|
+
const SETTLE_TIME_MS = 260
|
|
8
|
+
|
|
9
|
+
function clamp(value, min, max) {
|
|
10
|
+
return Math.min(max, Math.max(min, value))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createTerrainMotionAdapter({ document, window }) {
|
|
14
|
+
if (!document || !window) {
|
|
15
|
+
return { update() {}, dispose() {}, inspect: () => ({ current: 0, target: 0 }) }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let preferences = {}
|
|
19
|
+
let operation
|
|
20
|
+
let scroller
|
|
21
|
+
let layer
|
|
22
|
+
let observer
|
|
23
|
+
let frame
|
|
24
|
+
let mediaQuery
|
|
25
|
+
let mediaListener
|
|
26
|
+
let lastFrameTime = 0
|
|
27
|
+
let lastScroll = 0
|
|
28
|
+
let viewportMode
|
|
29
|
+
let current = 0
|
|
30
|
+
let target = 0
|
|
31
|
+
|
|
32
|
+
const motionReduced = () => preferences.motion === 'reduced' || Boolean(mediaQuery?.matches)
|
|
33
|
+
const phoneStatic = () => window.innerWidth <= PHONE_BREAKPOINT
|
|
34
|
+
const maxLag = () => window.innerWidth <= TABLET_BREAKPOINT ? TABLET_MAX_LAG : DESKTOP_MAX_LAG
|
|
35
|
+
const targetFor = scrollTop => phoneStatic() ? 0 : scrollTop * PARALLAX_RATIO
|
|
36
|
+
|
|
37
|
+
function paint() {
|
|
38
|
+
if (!layer) return
|
|
39
|
+
const wrapped = ((current % TILE_HEIGHT) + TILE_HEIGHT) % TILE_HEIGHT
|
|
40
|
+
layer.style.transform = `translate3d(0, -${wrapped.toFixed(3)}px, 0)`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cancelFrame() {
|
|
44
|
+
if (frame !== undefined) window.cancelAnimationFrame?.(frame)
|
|
45
|
+
frame = undefined
|
|
46
|
+
lastFrameTime = 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function animate(timestamp) {
|
|
50
|
+
frame = undefined
|
|
51
|
+
const elapsed = lastFrameTime ? Math.min(48, Math.max(1, timestamp - lastFrameTime)) : 16
|
|
52
|
+
lastFrameTime = timestamp
|
|
53
|
+
const response = SETTLE_TIME_MS / 3
|
|
54
|
+
const alpha = 1 - Math.exp(-elapsed / response)
|
|
55
|
+
current += (target - current) * alpha
|
|
56
|
+
if (Math.abs(target - current) < 0.08) current = target
|
|
57
|
+
paint()
|
|
58
|
+
if (current !== target) frame = window.requestAnimationFrame(animate)
|
|
59
|
+
else lastFrameTime = 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function snap() {
|
|
63
|
+
cancelFrame()
|
|
64
|
+
current = target
|
|
65
|
+
paint()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function schedule() {
|
|
69
|
+
if (current === target || frame !== undefined) return
|
|
70
|
+
frame = window.requestAnimationFrame(animate)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function syncFromScroll(forceSnap = false) {
|
|
74
|
+
if (!scroller) return
|
|
75
|
+
const nextScroll = Number(scroller.scrollTop) || 0
|
|
76
|
+
const delta = Math.abs(nextScroll - lastScroll)
|
|
77
|
+
lastScroll = nextScroll
|
|
78
|
+
target = targetFor(nextScroll)
|
|
79
|
+
const jumpThreshold = Math.max(720, (Number(scroller.clientHeight) || window.innerHeight || 800) * 0.9)
|
|
80
|
+
if (forceSnap || phoneStatic() || motionReduced() || delta >= jumpThreshold) {
|
|
81
|
+
snap()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const lag = maxLag()
|
|
85
|
+
current = clamp(current, target - lag, target + lag)
|
|
86
|
+
paint()
|
|
87
|
+
schedule()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function onScroll() {
|
|
91
|
+
syncFromScroll(false)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function unbindScroller() {
|
|
95
|
+
scroller?.removeEventListener('scroll', onScroll)
|
|
96
|
+
scroller = undefined
|
|
97
|
+
layer?.remove()
|
|
98
|
+
layer = undefined
|
|
99
|
+
cancelFrame()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function bindScroller(next) {
|
|
103
|
+
if (next === scroller && layer?.isConnected) return
|
|
104
|
+
unbindScroller()
|
|
105
|
+
scroller = next
|
|
106
|
+
if (!scroller || !operation) return
|
|
107
|
+
layer = document.createElement('div')
|
|
108
|
+
layer.setAttribute('data-prts-terrain-layer', '')
|
|
109
|
+
layer.setAttribute('aria-hidden', 'true')
|
|
110
|
+
operation.insertBefore(layer, operation.firstChild)
|
|
111
|
+
lastScroll = Number(scroller.scrollTop) || 0
|
|
112
|
+
target = targetFor(lastScroll)
|
|
113
|
+
current = target
|
|
114
|
+
paint()
|
|
115
|
+
scroller.addEventListener('scroll', onScroll, { passive: true })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function findScroller() {
|
|
119
|
+
const nextOperation = document.querySelector('[data-prts-region="operation"]')
|
|
120
|
+
if (nextOperation !== operation) {
|
|
121
|
+
unbindScroller()
|
|
122
|
+
observer?.disconnect()
|
|
123
|
+
operation = nextOperation
|
|
124
|
+
if (operation && window.MutationObserver) {
|
|
125
|
+
observer = new window.MutationObserver(findScroller)
|
|
126
|
+
observer.observe(operation, { childList: true, subtree: true })
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const nextScroller = operation?.querySelector('[data-conversation-scroll]')
|
|
130
|
+
bindScroller(nextScroller)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function onViewportChange() {
|
|
134
|
+
const nextMode = window.innerWidth <= PHONE_BREAKPOINT
|
|
135
|
+
? 'phone'
|
|
136
|
+
: window.innerWidth <= TABLET_BREAKPOINT ? 'tablet' : 'desktop'
|
|
137
|
+
if (nextMode === viewportMode) return
|
|
138
|
+
viewportMode = nextMode
|
|
139
|
+
syncFromScroll(true)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function attachMotionPreference() {
|
|
143
|
+
mediaQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)')
|
|
144
|
+
mediaListener = onViewportChange
|
|
145
|
+
mediaQuery?.addEventListener?.('change', mediaListener)
|
|
146
|
+
if (!mediaQuery?.addEventListener) mediaQuery?.addListener?.(mediaListener)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function detachMotionPreference() {
|
|
150
|
+
if (mediaQuery && mediaListener) {
|
|
151
|
+
mediaQuery.removeEventListener?.('change', mediaListener)
|
|
152
|
+
mediaQuery.removeListener?.(mediaListener)
|
|
153
|
+
}
|
|
154
|
+
mediaQuery = undefined
|
|
155
|
+
mediaListener = undefined
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
attachMotionPreference()
|
|
159
|
+
viewportMode = window.innerWidth <= PHONE_BREAKPOINT ? 'phone' : window.innerWidth <= TABLET_BREAKPOINT ? 'tablet' : 'desktop'
|
|
160
|
+
window.addEventListener?.('resize', onViewportChange, { passive: true })
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
update(nextPreferences = {}) {
|
|
164
|
+
preferences = nextPreferences
|
|
165
|
+
if (!preferences.enabled) {
|
|
166
|
+
unbindScroller()
|
|
167
|
+
observer?.disconnect()
|
|
168
|
+
observer = undefined
|
|
169
|
+
operation = undefined
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
findScroller()
|
|
173
|
+
if (layer) layer.hidden = preferences.texture === 'off'
|
|
174
|
+
syncFromScroll(true)
|
|
175
|
+
},
|
|
176
|
+
inspect() {
|
|
177
|
+
return { current, target, lastScroll, mounted: Boolean(layer?.isConnected) }
|
|
178
|
+
},
|
|
179
|
+
dispose() {
|
|
180
|
+
unbindScroller()
|
|
181
|
+
observer?.disconnect()
|
|
182
|
+
observer = undefined
|
|
183
|
+
operation = undefined
|
|
184
|
+
detachMotionPreference()
|
|
185
|
+
window.removeEventListener?.('resize', onViewportChange)
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { normalizePreferences } from './preferences.js'
|
|
2
|
+
|
|
3
|
+
const ROOT_ATTRIBUTES = [
|
|
4
|
+
'data-dsh-prts',
|
|
5
|
+
'data-prts-scheme',
|
|
6
|
+
'data-prts-texture',
|
|
7
|
+
'data-prts-glass',
|
|
8
|
+
'data-prts-motion',
|
|
9
|
+
'data-prts-particle-pattern',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
function themeIdentity(value) {
|
|
13
|
+
if (typeof value === 'string') return value
|
|
14
|
+
if (!value || typeof value !== 'object') return ''
|
|
15
|
+
const active = value.active
|
|
16
|
+
if (active && typeof active === 'object') {
|
|
17
|
+
const resolved = active.colorScheme || active.scheme || active.appearance || active.mode || active.id || active.name
|
|
18
|
+
if (resolved) return resolved
|
|
19
|
+
}
|
|
20
|
+
return value.colorScheme || value.scheme || value.appearance || value.mode || value.id || value.name || value.preference || ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveScheme(value) {
|
|
24
|
+
const identity = String(themeIdentity(value)).toLowerCase()
|
|
25
|
+
return identity.includes('dark') || identity.includes('night') ? 'dark' : 'light'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createThemeController({ document, window, cssText, service }) {
|
|
29
|
+
if (!document || !window) return { apply() {}, sync() {}, refresh() {}, setTheme() {}, toggle() {}, dispose() {} }
|
|
30
|
+
|
|
31
|
+
const root = document.documentElement
|
|
32
|
+
let style
|
|
33
|
+
let disposed = false
|
|
34
|
+
let refreshRevision = 0
|
|
35
|
+
let setRevision = 0
|
|
36
|
+
|
|
37
|
+
function clearThemeState() {
|
|
38
|
+
for (const attribute of ROOT_ATTRIBUTES) root.removeAttribute(attribute)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function removeOwnedState() {
|
|
42
|
+
disposed = true
|
|
43
|
+
refreshRevision += 1
|
|
44
|
+
setRevision += 1
|
|
45
|
+
clearThemeState()
|
|
46
|
+
root.removeAttribute('data-dsh-prts-settings')
|
|
47
|
+
style?.remove()
|
|
48
|
+
style = undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensureStyle() {
|
|
52
|
+
if (!style?.isConnected) {
|
|
53
|
+
style = document.createElement('style')
|
|
54
|
+
style.dataset.plugin = 'dsh-theme-prts'
|
|
55
|
+
style.dataset.pluginCss = 'dsh-theme-prts/prts.css'
|
|
56
|
+
document.head.appendChild(style)
|
|
57
|
+
}
|
|
58
|
+
if (style.textContent !== cssText) style.textContent = cssText
|
|
59
|
+
root.setAttribute('data-dsh-prts-settings', '')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sync(value) {
|
|
63
|
+
if (disposed) return
|
|
64
|
+
root.dataset.prtsScheme = resolveScheme(value)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fallbackTheme() {
|
|
68
|
+
return root.dataset.theme || root.getAttribute('data-color-scheme') || root.style.colorScheme || 'light'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function refresh() {
|
|
72
|
+
const revision = ++refreshRevision
|
|
73
|
+
let value
|
|
74
|
+
try { value = service?.getTheme?.() } catch { value = undefined }
|
|
75
|
+
if (value && typeof value.then === 'function') {
|
|
76
|
+
return value.then(next => {
|
|
77
|
+
if (!disposed && revision === refreshRevision) sync(next)
|
|
78
|
+
return next
|
|
79
|
+
}).catch(() => {
|
|
80
|
+
if (!disposed && revision === refreshRevision) sync(fallbackTheme())
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
sync(value ?? fallbackTheme())
|
|
84
|
+
return value
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function setTheme(id) {
|
|
88
|
+
const target = id === 'dark' ? 'dark' : 'light'
|
|
89
|
+
const revision = ++setRevision
|
|
90
|
+
try {
|
|
91
|
+
const result = service?.setTheme?.(target)
|
|
92
|
+
if (result && typeof result.then === 'function') {
|
|
93
|
+
return result.then(() => {
|
|
94
|
+
if (!disposed && revision === setRevision) sync(target)
|
|
95
|
+
return target
|
|
96
|
+
}).catch(() => {
|
|
97
|
+
if (!disposed && revision === setRevision) refresh()
|
|
98
|
+
return root.dataset.prtsScheme
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
if (!disposed && revision === setRevision) sync(target)
|
|
102
|
+
} catch {
|
|
103
|
+
if (!disposed && revision === setRevision) sync(target)
|
|
104
|
+
}
|
|
105
|
+
return target
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toggle() {
|
|
109
|
+
return setTheme(root.dataset.prtsScheme === 'dark' ? 'light' : 'dark')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
apply(value) {
|
|
114
|
+
disposed = false
|
|
115
|
+
const preferences = normalizePreferences(value)
|
|
116
|
+
ensureStyle()
|
|
117
|
+
if (!preferences.enabled) {
|
|
118
|
+
clearThemeState()
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
root.setAttribute('data-dsh-prts', '')
|
|
122
|
+
root.dataset.prtsTexture = preferences.texture
|
|
123
|
+
root.dataset.prtsGlass = preferences.glass
|
|
124
|
+
root.dataset.prtsMotion = preferences.motion
|
|
125
|
+
root.dataset.prtsParticlePattern = 'orthogonal'
|
|
126
|
+
refresh()
|
|
127
|
+
},
|
|
128
|
+
sync,
|
|
129
|
+
refresh,
|
|
130
|
+
setTheme,
|
|
131
|
+
toggle,
|
|
132
|
+
dispose: removeOwnedState,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { normalizePreferences } from './preferences.js'
|
|
2
|
+
|
|
3
|
+
const ROOT_ATTRIBUTES = [
|
|
4
|
+
'data-dsh-prts',
|
|
5
|
+
'data-prts-scheme',
|
|
6
|
+
'data-prts-texture',
|
|
7
|
+
'data-prts-glass',
|
|
8
|
+
'data-prts-motion',
|
|
9
|
+
'data-prts-particle-pattern',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
function themeIdentity(value) {
|
|
13
|
+
if (typeof value === 'string') return value
|
|
14
|
+
if (!value || typeof value !== 'object') return ''
|
|
15
|
+
const active = value.active
|
|
16
|
+
if (active && typeof active === 'object') {
|
|
17
|
+
const resolved = active.colorScheme || active.scheme || active.appearance || active.mode || active.id || active.name
|
|
18
|
+
if (resolved) return resolved
|
|
19
|
+
}
|
|
20
|
+
return value.colorScheme || value.scheme || value.appearance || value.mode || value.id || value.name || value.preference || ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveScheme(value) {
|
|
24
|
+
const identity = String(themeIdentity(value)).toLowerCase()
|
|
25
|
+
return identity.includes('dark') || identity.includes('night') ? 'dark' : 'light'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createThemeController({ document, window, cssText, service }) {
|
|
29
|
+
if (!document || !window) return { apply() {}, sync() {}, refresh() {}, setTheme() {}, toggle() {}, dispose() {} }
|
|
30
|
+
|
|
31
|
+
const root = document.documentElement
|
|
32
|
+
let style
|
|
33
|
+
let disposed = false
|
|
34
|
+
let refreshRevision = 0
|
|
35
|
+
|
|
36
|
+
function clearThemeState() {
|
|
37
|
+
for (const attribute of ROOT_ATTRIBUTES) root.removeAttribute(attribute)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function removeOwnedState() {
|
|
41
|
+
disposed = true
|
|
42
|
+
refreshRevision += 1
|
|
43
|
+
clearThemeState()
|
|
44
|
+
root.removeAttribute('data-dsh-prts-settings')
|
|
45
|
+
style?.remove()
|
|
46
|
+
style = undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureStyle() {
|
|
50
|
+
if (!style?.isConnected) {
|
|
51
|
+
style = document.createElement('style')
|
|
52
|
+
style.dataset.plugin = 'dsh-theme-prts'
|
|
53
|
+
style.dataset.pluginCss = 'dsh-theme-prts/prts.css'
|
|
54
|
+
document.head.appendChild(style)
|
|
55
|
+
}
|
|
56
|
+
if (style.textContent !== cssText) style.textContent = cssText
|
|
57
|
+
root.setAttribute('data-dsh-prts-settings', '')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sync(value) {
|
|
61
|
+
if (disposed) return
|
|
62
|
+
root.dataset.prtsScheme = resolveScheme(value)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fallbackTheme() {
|
|
66
|
+
return root.dataset.theme || root.getAttribute('data-color-scheme') || root.style.colorScheme || 'light'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function refresh() {
|
|
70
|
+
const revision = ++refreshRevision
|
|
71
|
+
let value
|
|
72
|
+
try { value = service?.getTheme?.() } catch { value = undefined }
|
|
73
|
+
if (value && typeof value.then === 'function') {
|
|
74
|
+
value.then(next => {
|
|
75
|
+
if (!disposed && revision === refreshRevision) sync(next)
|
|
76
|
+
}).catch(() => {
|
|
77
|
+
if (!disposed && revision === refreshRevision) sync(fallbackTheme())
|
|
78
|
+
})
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
sync(value ?? fallbackTheme())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function setTheme(id) {
|
|
85
|
+
const target = id === 'dark' ? 'dark' : 'light'
|
|
86
|
+
sync(target)
|
|
87
|
+
try {
|
|
88
|
+
const result = service?.setTheme?.(target)
|
|
89
|
+
result?.then?.(() => refresh()).catch?.(() => {})
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function toggle() {
|
|
94
|
+
setTheme(root.dataset.prtsScheme === 'dark' ? 'light' : 'dark')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
apply(value) {
|
|
99
|
+
disposed = false
|
|
100
|
+
const preferences = normalizePreferences(value)
|
|
101
|
+
ensureStyle()
|
|
102
|
+
if (!preferences.enabled) {
|
|
103
|
+
clearThemeState()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
root.setAttribute('data-dsh-prts', '')
|
|
107
|
+
root.dataset.prtsTexture = preferences.texture
|
|
108
|
+
root.dataset.prtsGlass = preferences.glass
|
|
109
|
+
root.dataset.prtsMotion = preferences.motion
|
|
110
|
+
root.dataset.prtsParticlePattern = 'orthogonal'
|
|
111
|
+
refresh()
|
|
112
|
+
},
|
|
113
|
+
sync,
|
|
114
|
+
refresh,
|
|
115
|
+
setTheme,
|
|
116
|
+
toggle,
|
|
117
|
+
dispose: removeOwnedState,
|
|
118
|
+
}
|
|
119
|
+
}
|