@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,317 @@
|
|
|
1
|
+
import { ensureFacilityGraphic, ensureFacilityTextureDefs, renderFacilityGraphic } from './facility-vector.js'
|
|
2
|
+
|
|
3
|
+
const OWNED_ATTRIBUTES = [
|
|
4
|
+
'data-prts-row-title',
|
|
5
|
+
'data-prts-workspace-disclosure',
|
|
6
|
+
'data-prts-workspace-icon',
|
|
7
|
+
'data-prts-row-menu-open',
|
|
8
|
+
'data-prts-workspace-row',
|
|
9
|
+
'data-prts-workspace-actions',
|
|
10
|
+
'data-prts-workspace-menu-anchor',
|
|
11
|
+
'data-prts-workspace-menu',
|
|
12
|
+
'data-prts-workspace-create',
|
|
13
|
+
'data-prts-session-row',
|
|
14
|
+
'data-prts-session-actions',
|
|
15
|
+
'data-prts-session-menu-anchor',
|
|
16
|
+
'data-prts-session-menu',
|
|
17
|
+
'data-prts-session-time',
|
|
18
|
+
'data-prts-session-index',
|
|
19
|
+
'data-prts-facility-face',
|
|
20
|
+
'data-prts-facility-spine',
|
|
21
|
+
'data-prts-facility-vector',
|
|
22
|
+
'data-prts-facility-size',
|
|
23
|
+
'data-prts-facility-texture',
|
|
24
|
+
'data-prts-spine-vector',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
function markSidebarNode(node, attribute, next) {
|
|
28
|
+
if (!node) return
|
|
29
|
+
node.setAttribute(attribute, '')
|
|
30
|
+
next.add(node)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveRowControls(row, kind) {
|
|
34
|
+
const menu = row.querySelector(':scope > span > span > button[type="button"]')
|
|
35
|
+
const anchor = menu?.parentElement
|
|
36
|
+
const actions = anchor?.parentElement
|
|
37
|
+
if (!menu || !anchor || actions?.parentElement !== row) return {}
|
|
38
|
+
const create = kind === 'workspace'
|
|
39
|
+
? actions.querySelector(':scope > button[type="button"]')
|
|
40
|
+
: undefined
|
|
41
|
+
return { actions, anchor, create, menu }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveRowTitle(row, kind, actions) {
|
|
45
|
+
const candidates = Array.from(row.querySelectorAll(':scope > span')).filter(node => (
|
|
46
|
+
node !== actions
|
|
47
|
+
&& !node.hasAttribute('data-prts-row-projection')
|
|
48
|
+
&& node.getAttribute('aria-hidden') !== 'true'
|
|
49
|
+
&& !node.querySelector('button')
|
|
50
|
+
&& node.textContent.trim()
|
|
51
|
+
))
|
|
52
|
+
if (kind === 'session') return candidates.find(node => node.matches('[class*="title"], [data-session-title]')) ?? candidates.at(-1)
|
|
53
|
+
return candidates.find(node => node.matches('[class*="project"], [class*="workspace"], [data-workspace-title]')) ?? candidates.at(-1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveWorkspaceIcon(row, title, actions) {
|
|
57
|
+
return Array.from(row.children).find(node => (
|
|
58
|
+
node.tagName === 'SPAN'
|
|
59
|
+
&& node !== title
|
|
60
|
+
&& node !== actions
|
|
61
|
+
&& !node.hasAttribute('data-prts-facility-face')
|
|
62
|
+
&& !node.hasAttribute('data-prts-facility-spine')
|
|
63
|
+
&& !node.hasAttribute('data-prts-row-projection')
|
|
64
|
+
))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveWorkspaceDisclosure(row, title, actions, icon) {
|
|
68
|
+
return Array.from(row.children).find(node => (
|
|
69
|
+
node.tagName === 'SPAN'
|
|
70
|
+
&& node !== title
|
|
71
|
+
&& node !== actions
|
|
72
|
+
&& node !== icon
|
|
73
|
+
&& !node.hasAttribute('data-prts-facility-face')
|
|
74
|
+
&& !node.hasAttribute('data-prts-facility-spine')
|
|
75
|
+
&& !node.hasAttribute('data-prts-row-projection')
|
|
76
|
+
&& !node.querySelector('button')
|
|
77
|
+
))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveSessionTime(row, title, actions) {
|
|
81
|
+
const candidates = Array.from(row.querySelectorAll(':scope > span')).filter(node => (
|
|
82
|
+
node !== title
|
|
83
|
+
&& node !== actions
|
|
84
|
+
&& !node.hasAttribute('data-prts-facility-face')
|
|
85
|
+
&& !node.hasAttribute('data-prts-row-projection')
|
|
86
|
+
&& node.getAttribute('aria-hidden') !== 'true'
|
|
87
|
+
&& !node.querySelector('button')
|
|
88
|
+
&& node.textContent.trim()
|
|
89
|
+
))
|
|
90
|
+
return candidates.find(node => node.matches('[class*="time" i], time, [data-session-time]')) ?? candidates.at(-1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ensureFacilityFace(document, row, kind, next) {
|
|
94
|
+
let face = row.querySelector(':scope > [data-prts-facility-face]')
|
|
95
|
+
if (!face) {
|
|
96
|
+
face = document.createElement('span')
|
|
97
|
+
face.setAttribute('data-prts-owned-facility', '')
|
|
98
|
+
row.insertBefore(face, row.firstChild)
|
|
99
|
+
}
|
|
100
|
+
face.setAttribute('data-prts-facility-face', kind)
|
|
101
|
+
face.setAttribute('data-prts-facility-texture', kind === 'workspace' ? 'silhouette' : 'pickup')
|
|
102
|
+
face.setAttribute('aria-hidden', 'true')
|
|
103
|
+
next.add(face)
|
|
104
|
+
return face
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ensureFacilitySpine(document, row, kind, next) {
|
|
108
|
+
let spine = row.querySelector(':scope > [data-prts-facility-spine]')
|
|
109
|
+
if (!spine) {
|
|
110
|
+
spine = document.createElement('span')
|
|
111
|
+
spine.setAttribute('data-prts-owned-facility-spine', '')
|
|
112
|
+
row.insertBefore(spine, row.firstChild)
|
|
113
|
+
}
|
|
114
|
+
spine.setAttribute('data-prts-facility-spine', kind)
|
|
115
|
+
spine.setAttribute('aria-hidden', 'true')
|
|
116
|
+
next.add(spine)
|
|
117
|
+
return spine
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
export function createSidebarControlAdapter({ document, window }) {
|
|
122
|
+
let observer
|
|
123
|
+
let resizeObserver
|
|
124
|
+
let resizeFrame
|
|
125
|
+
let resizeTimer
|
|
126
|
+
let scanFrame
|
|
127
|
+
let owned = new Set()
|
|
128
|
+
let vectorOwners = new Set()
|
|
129
|
+
const dirtyVectors = new Map()
|
|
130
|
+
const vectorSizes = new WeakMap()
|
|
131
|
+
|
|
132
|
+
function scheduleVectors() {
|
|
133
|
+
if (resizeFrame !== undefined) return
|
|
134
|
+
const requestFrame = window?.requestAnimationFrame?.bind(window) ?? (callback => window?.setTimeout?.(callback, 0))
|
|
135
|
+
resizeFrame = requestFrame?.(flushVectors)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function flushVectors() {
|
|
139
|
+
resizeFrame = undefined
|
|
140
|
+
const measurements = []
|
|
141
|
+
for (const [owner, observedSize] of dirtyVectors) {
|
|
142
|
+
if (!vectorOwners.has(owner) || !owner.isConnected) continue
|
|
143
|
+
let size = observedSize
|
|
144
|
+
if (!size) {
|
|
145
|
+
const box = owner.getBoundingClientRect?.()
|
|
146
|
+
size = {
|
|
147
|
+
width: owner.clientWidth || owner.offsetWidth || box?.width || 0,
|
|
148
|
+
height: owner.clientHeight || owner.offsetHeight || box?.height || 0,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const previous = vectorSizes.get(owner)
|
|
152
|
+
if (previous?.width === size.width && previous?.height === size.height) continue
|
|
153
|
+
vectorSizes.set(owner, size)
|
|
154
|
+
measurements.push([owner, size])
|
|
155
|
+
}
|
|
156
|
+
dirtyVectors.clear()
|
|
157
|
+
for (const [owner, size] of measurements) renderFacilityGraphic(owner, size)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function registerVector(owner, options, next, nextVectorOwners) {
|
|
161
|
+
if (!owner) return
|
|
162
|
+
const graphic = ensureFacilityGraphic(document, owner, options)
|
|
163
|
+
if (!graphic) return
|
|
164
|
+
next.add(graphic)
|
|
165
|
+
nextVectorOwners.add(owner)
|
|
166
|
+
if (!vectorOwners.has(owner)) resizeObserver?.observe(owner)
|
|
167
|
+
if (!vectorSizes.has(owner)) dirtyVectors.set(owner, undefined)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function syncVectorOwners(next) {
|
|
171
|
+
for (const owner of vectorOwners) {
|
|
172
|
+
if (next.has(owner)) continue
|
|
173
|
+
resizeObserver?.unobserve?.(owner)
|
|
174
|
+
dirtyVectors.delete(owner)
|
|
175
|
+
}
|
|
176
|
+
vectorOwners = next
|
|
177
|
+
scheduleVectors()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
function queueScan() {
|
|
182
|
+
if (scanFrame !== undefined) return
|
|
183
|
+
const requestFrame = window?.requestAnimationFrame?.bind(window) ?? (callback => window?.setTimeout?.(callback, 0))
|
|
184
|
+
scanFrame = requestFrame(() => {
|
|
185
|
+
scanFrame = undefined
|
|
186
|
+
scan()
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function scan() {
|
|
191
|
+
const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
|
|
192
|
+
const next = new Set()
|
|
193
|
+
const nextVectorOwners = new Set()
|
|
194
|
+
if (root) {
|
|
195
|
+
for (const projection of root.querySelectorAll('[data-prts-owned-projection]')) projection.remove()
|
|
196
|
+
for (const row of root.querySelectorAll('div[role="treeitem"][aria-expanded]')) {
|
|
197
|
+
const controls = resolveRowControls(row, 'workspace')
|
|
198
|
+
const face = ensureFacilityFace(document, row, 'workspace', next)
|
|
199
|
+
const spine = ensureFacilitySpine(document, row, 'workspace', next)
|
|
200
|
+
registerVector(face, { kind: 'face', topNotch: true, texture: 'silhouette' }, next, nextVectorOwners)
|
|
201
|
+
registerVector(spine, { kind: 'spine' }, next, nextVectorOwners)
|
|
202
|
+
markSidebarNode(row, 'data-prts-workspace-row', next)
|
|
203
|
+
const title = resolveRowTitle(row, 'workspace', controls.actions)
|
|
204
|
+
markSidebarNode(title, 'data-prts-row-title', next)
|
|
205
|
+
const icon = resolveWorkspaceIcon(row, title, controls.actions)
|
|
206
|
+
markSidebarNode(icon, 'data-prts-workspace-icon', next)
|
|
207
|
+
markSidebarNode(resolveWorkspaceDisclosure(row, title, controls.actions, icon), 'data-prts-workspace-disclosure', next)
|
|
208
|
+
row.toggleAttribute('data-prts-row-menu-open', controls.menu?.getAttribute('aria-expanded') === 'true'
|
|
209
|
+
|| row.classList.contains('menuOpen'))
|
|
210
|
+
markSidebarNode(controls.actions, 'data-prts-workspace-actions', next)
|
|
211
|
+
markSidebarNode(controls.anchor, 'data-prts-workspace-menu-anchor', next)
|
|
212
|
+
markSidebarNode(controls.menu, 'data-prts-workspace-menu', next)
|
|
213
|
+
markSidebarNode(controls.create, 'data-prts-workspace-create', next)
|
|
214
|
+
registerVector(controls.menu, { kind: 'button' }, next, nextVectorOwners)
|
|
215
|
+
registerVector(controls.create, { kind: 'button' }, next, nextVectorOwners)
|
|
216
|
+
}
|
|
217
|
+
let sessionOrdinal = 0
|
|
218
|
+
for (const row of root.querySelectorAll('div[role="treeitem"][aria-selected]')) {
|
|
219
|
+
const sessionIndex = String(++sessionOrdinal).padStart(2, '0')
|
|
220
|
+
const controls = resolveRowControls(row, 'session')
|
|
221
|
+
const face = ensureFacilityFace(document, row, 'session', next)
|
|
222
|
+
const spine = ensureFacilitySpine(document, row, 'session', next)
|
|
223
|
+
registerVector(face, { kind: 'face', topNotch: true, texture: 'pickup' }, next, nextVectorOwners)
|
|
224
|
+
registerVector(spine, { kind: 'spine' }, next, nextVectorOwners)
|
|
225
|
+
markSidebarNode(row, 'data-prts-session-row', next)
|
|
226
|
+
row.dataset.prtsSessionIndex = sessionIndex
|
|
227
|
+
const title = resolveRowTitle(row, 'session', controls.actions)
|
|
228
|
+
markSidebarNode(title, 'data-prts-row-title', next)
|
|
229
|
+
markSidebarNode(resolveSessionTime(row, title, controls.actions), 'data-prts-session-time', next)
|
|
230
|
+
if (title) title.dataset.prtsSessionIndex = sessionIndex
|
|
231
|
+
row.toggleAttribute('data-prts-row-menu-open', controls.menu?.getAttribute('aria-expanded') === 'true'
|
|
232
|
+
|| row.classList.contains('menuOpen'))
|
|
233
|
+
markSidebarNode(controls.actions, 'data-prts-session-actions', next)
|
|
234
|
+
markSidebarNode(controls.anchor, 'data-prts-session-menu-anchor', next)
|
|
235
|
+
markSidebarNode(controls.menu, 'data-prts-session-menu', next)
|
|
236
|
+
registerVector(controls.menu, { kind: 'button' }, next, nextVectorOwners)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const node of owned) {
|
|
240
|
+
if (next.has(node)) continue
|
|
241
|
+
if (node.hasAttribute?.('data-prts-owned-projection')
|
|
242
|
+
|| node.hasAttribute?.('data-prts-owned-facility')
|
|
243
|
+
|| node.hasAttribute?.('data-prts-owned-facility-spine')
|
|
244
|
+
|| node.hasAttribute?.('data-prts-owned-facility-vector')) {
|
|
245
|
+
node.remove()
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
for (const attribute of OWNED_ATTRIBUTES) node.removeAttribute?.(attribute)
|
|
249
|
+
}
|
|
250
|
+
syncVectorOwners(nextVectorOwners)
|
|
251
|
+
owned = next
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function clear() {
|
|
255
|
+
for (const node of document?.querySelectorAll?.('[data-prts-owned-projection], [data-prts-owned-facility], [data-prts-owned-facility-spine], [data-prts-owned-facility-vector], [data-prts-owned-facility-defs]') ?? []) node.remove()
|
|
256
|
+
for (const node of owned) {
|
|
257
|
+
for (const attribute of OWNED_ATTRIBUTES) node.removeAttribute?.(attribute)
|
|
258
|
+
}
|
|
259
|
+
for (const attribute of OWNED_ATTRIBUTES) {
|
|
260
|
+
for (const node of document?.querySelectorAll?.(`[${attribute}]`) ?? []) node.removeAttribute(attribute)
|
|
261
|
+
}
|
|
262
|
+
vectorOwners.clear()
|
|
263
|
+
dirtyVectors.clear()
|
|
264
|
+
owned.clear()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
start() {
|
|
269
|
+
if (!document) return
|
|
270
|
+
ensureFacilityTextureDefs(document)
|
|
271
|
+
const ResizeObserver = window?.ResizeObserver
|
|
272
|
+
if (!resizeObserver && typeof ResizeObserver === 'function') {
|
|
273
|
+
resizeObserver = new ResizeObserver(entries => {
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
const rect = entry.contentRect
|
|
276
|
+
dirtyVectors.set(entry.target, rect ? { width: rect.width, height: rect.height } : undefined)
|
|
277
|
+
}
|
|
278
|
+
if (resizeTimer !== undefined) window?.clearTimeout?.(resizeTimer)
|
|
279
|
+
resizeTimer = window?.setTimeout?.(() => {
|
|
280
|
+
resizeTimer = undefined
|
|
281
|
+
scheduleVectors()
|
|
282
|
+
}, 160)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
scan()
|
|
286
|
+
if (observer || !window?.MutationObserver) return
|
|
287
|
+
const observationRoot = document.querySelector('[data-prts-region="sessions"]')
|
|
288
|
+
if (!observationRoot) return
|
|
289
|
+
observer = new window.MutationObserver(queueScan)
|
|
290
|
+
observer.observe(observationRoot, {
|
|
291
|
+
subtree: true,
|
|
292
|
+
childList: true,
|
|
293
|
+
attributes: true,
|
|
294
|
+
attributeFilter: ['aria-expanded', 'aria-selected', 'class'],
|
|
295
|
+
})
|
|
296
|
+
},
|
|
297
|
+
dispose() {
|
|
298
|
+
observer?.disconnect()
|
|
299
|
+
observer = undefined
|
|
300
|
+
if (scanFrame !== undefined) {
|
|
301
|
+
if (window?.cancelAnimationFrame) window.cancelAnimationFrame(scanFrame)
|
|
302
|
+
else window?.clearTimeout?.(scanFrame)
|
|
303
|
+
}
|
|
304
|
+
scanFrame = undefined
|
|
305
|
+
resizeObserver?.disconnect()
|
|
306
|
+
resizeObserver = undefined
|
|
307
|
+
if (resizeFrame !== undefined) {
|
|
308
|
+
if (window?.cancelAnimationFrame) window.cancelAnimationFrame(resizeFrame)
|
|
309
|
+
else window?.clearTimeout?.(resizeFrame)
|
|
310
|
+
}
|
|
311
|
+
resizeFrame = undefined
|
|
312
|
+
if (resizeTimer !== undefined) window?.clearTimeout?.(resizeTimer)
|
|
313
|
+
resizeTimer = undefined
|
|
314
|
+
clear()
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
export const PRTS_STARTUP_TIMINGS = Object.freeze({
|
|
2
|
+
enter: 20,
|
|
3
|
+
minimum: 1350,
|
|
4
|
+
intro: 560,
|
|
5
|
+
approach: 2400,
|
|
6
|
+
finish: 260,
|
|
7
|
+
copyFade: 90,
|
|
8
|
+
readyHold: 280,
|
|
9
|
+
exitLead: 120,
|
|
10
|
+
exit: 470,
|
|
11
|
+
completePadding: 40,
|
|
12
|
+
reducedExit: 160,
|
|
13
|
+
timeout: 20000,
|
|
14
|
+
timeoutHold: 700,
|
|
15
|
+
watchdog: 22000,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const STARTUP_STAGES = Object.freeze({
|
|
19
|
+
linking: Object.freeze({ label: 'LINKING', detail: '神经链路接入' }),
|
|
20
|
+
authenticating: Object.freeze({ label: 'AUTHENTICATING', detail: '博士身份校验' }),
|
|
21
|
+
visualOnline: Object.freeze({ label: 'VISUAL LAYER ONLINE', detail: '视觉层已上线' }),
|
|
22
|
+
ready: Object.freeze({ label: 'P.R.T.S. READY', detail: '终端接管完成' }),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function startupMarkup(prtsEmblem = '', rhodesEmblem = '') {
|
|
26
|
+
const ticks = Array.from({ length: 21 }, (_, index) => `<i${index % 5 === 0 ? ' data-major' : ''}></i>`).join('')
|
|
27
|
+
return `<section data-prts-startup data-stage="boot" popover="manual" role="status" aria-live="assertive" aria-label="P.R.T.S. 启动序列" tabindex="-1">
|
|
28
|
+
<div data-prts-startup-grid aria-hidden="true"></div>
|
|
29
|
+
<div data-prts-startup-scan aria-hidden="true"></div>
|
|
30
|
+
<div data-prts-startup-cut="left" aria-hidden="true"></div>
|
|
31
|
+
<div data-prts-startup-cut="right" aria-hidden="true"></div>
|
|
32
|
+
<header data-prts-startup-header aria-hidden="true">
|
|
33
|
+
<span>P.R.T.S. // RHODES ISLAND</span>
|
|
34
|
+
<small>NEURAL TERMINAL CONNECTION</small>
|
|
35
|
+
</header>
|
|
36
|
+
<aside data-prts-startup-telemetry aria-hidden="true">
|
|
37
|
+
<span>ORIGIN // RI-001</span>
|
|
38
|
+
<span>PROTOCOL // TACTICAL</span>
|
|
39
|
+
<span>LINK // ENCRYPTED</span>
|
|
40
|
+
</aside>
|
|
41
|
+
<main data-prts-startup-core>
|
|
42
|
+
<div data-prts-startup-frame aria-hidden="true">
|
|
43
|
+
<span data-prts-startup-frame-outer></span>
|
|
44
|
+
<span data-prts-startup-frame-inner></span>
|
|
45
|
+
<span data-prts-startup-reticle></span>
|
|
46
|
+
<img data-prts-startup-emblem src="${prtsEmblem}" alt="">
|
|
47
|
+
</div>
|
|
48
|
+
<div data-prts-startup-signature aria-hidden="true">
|
|
49
|
+
<span>${rhodesEmblem}</span>
|
|
50
|
+
<small>RHODES ISLAND<br>RI-001</small>
|
|
51
|
+
</div>
|
|
52
|
+
<div data-prts-startup-state>
|
|
53
|
+
<strong data-prts-startup-label>INITIALIZING</strong>
|
|
54
|
+
<span data-prts-startup-detail>启动序列初始化</span>
|
|
55
|
+
</div>
|
|
56
|
+
</main>
|
|
57
|
+
<footer data-prts-startup-progress>
|
|
58
|
+
<output aria-hidden="true">BOOT SEQUENCE // <span data-prts-startup-percent>000%</span></output>
|
|
59
|
+
<div data-prts-startup-track aria-hidden="true">
|
|
60
|
+
<b data-prts-startup-fill></b>
|
|
61
|
+
<span data-prts-startup-ticks>${ticks}</span>
|
|
62
|
+
</div>
|
|
63
|
+
</footer>
|
|
64
|
+
<div data-prts-startup-coordinate aria-hidden="true">X 061.38 Y 109.42</div>
|
|
65
|
+
</section>`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clamp01(value) {
|
|
69
|
+
return Math.max(0, Math.min(1, value))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function easeOutCubic(value) {
|
|
73
|
+
return 1 - Math.pow(1 - clamp01(value), 3)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function findNativeHarnessLoader(document) {
|
|
77
|
+
return document?.querySelector?.('[data-dsh-boot]') ?? null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createPrtsStartupSequence({
|
|
81
|
+
document,
|
|
82
|
+
window,
|
|
83
|
+
prtsEmblem = '',
|
|
84
|
+
rhodesEmblem = '',
|
|
85
|
+
timings = PRTS_STARTUP_TIMINGS,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const schedule = { ...PRTS_STARTUP_TIMINGS, ...timings }
|
|
88
|
+
let overlay
|
|
89
|
+
let frame
|
|
90
|
+
let inertRecords = []
|
|
91
|
+
let previousFocus
|
|
92
|
+
let startedAt
|
|
93
|
+
let waitForReady = false
|
|
94
|
+
let readyWhen
|
|
95
|
+
let hostReady = false
|
|
96
|
+
let finishStartedAt
|
|
97
|
+
let finishStartedProgress = 0
|
|
98
|
+
let readyStartedAt
|
|
99
|
+
let exitStartedAt
|
|
100
|
+
let timeoutStartedAt
|
|
101
|
+
let currentStage = 'boot'
|
|
102
|
+
let pendingStage
|
|
103
|
+
let copyTransitionStartedAt
|
|
104
|
+
let lastProgress = -1
|
|
105
|
+
let useReducedMotion = false
|
|
106
|
+
|
|
107
|
+
const now = () => window?.performance?.now?.() ?? Date.now()
|
|
108
|
+
const requestFrame = callback => window?.requestAnimationFrame?.(callback)
|
|
109
|
+
?? window?.setTimeout?.(() => callback(now()), 16)
|
|
110
|
+
const cancelFrame = id => {
|
|
111
|
+
if (id === undefined) return
|
|
112
|
+
if (window?.cancelAnimationFrame) window.cancelAnimationFrame(id)
|
|
113
|
+
else window?.clearTimeout?.(id)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function restoreInteractivity() {
|
|
117
|
+
for (const record of inertRecords) {
|
|
118
|
+
if (!record.node?.isConnected) continue
|
|
119
|
+
record.node.inert = record.inert
|
|
120
|
+
if (record.hadAttribute) record.node.setAttribute('inert', '')
|
|
121
|
+
else record.node.removeAttribute('inert')
|
|
122
|
+
}
|
|
123
|
+
inertRecords = []
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cleanup({ restoreFocus = true } = {}) {
|
|
127
|
+
cancelFrame(frame)
|
|
128
|
+
frame = undefined
|
|
129
|
+
document?.removeEventListener?.('visibilitychange', onVisibilityChange)
|
|
130
|
+
document?.documentElement?.removeAttribute?.('data-prts-startup-active')
|
|
131
|
+
try {
|
|
132
|
+
overlay?.hidePopover?.()
|
|
133
|
+
} catch {
|
|
134
|
+
// The overlay may already have left the top layer during page teardown.
|
|
135
|
+
}
|
|
136
|
+
overlay?.remove?.()
|
|
137
|
+
overlay = undefined
|
|
138
|
+
restoreInteractivity()
|
|
139
|
+
if (restoreFocus && previousFocus?.isConnected) previousFocus.focus?.()
|
|
140
|
+
previousFocus = undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function updateProgress(progress) {
|
|
144
|
+
if (!overlay) return
|
|
145
|
+
const safeProgress = Math.max(0, Math.min(100, progress))
|
|
146
|
+
if (Math.abs(safeProgress - lastProgress) < .01) return
|
|
147
|
+
lastProgress = safeProgress
|
|
148
|
+
overlay.style.setProperty('--prts-startup-progress', (safeProgress / 100).toFixed(4))
|
|
149
|
+
const percent = overlay.querySelector('[data-prts-startup-percent]')
|
|
150
|
+
if (percent) percent.textContent = `${String(Math.round(safeProgress)).padStart(3, '0')}%`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function applyStage(name) {
|
|
154
|
+
if (!overlay) return
|
|
155
|
+
const stage = STARTUP_STAGES[name]
|
|
156
|
+
if (!stage) return
|
|
157
|
+
currentStage = name
|
|
158
|
+
overlay.dataset.stage = name
|
|
159
|
+
const label = overlay.querySelector('[data-prts-startup-label]')
|
|
160
|
+
const detail = overlay.querySelector('[data-prts-startup-detail]')
|
|
161
|
+
if (label) label.textContent = stage.label
|
|
162
|
+
if (detail) detail.textContent = stage.detail
|
|
163
|
+
overlay.setAttribute('aria-label', `P.R.T.S. 启动序列:${stage.detail}`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function requestStage(name, elapsed) {
|
|
167
|
+
if (!overlay || name === currentStage && !pendingStage) return
|
|
168
|
+
if (!pendingStage) {
|
|
169
|
+
copyTransitionStartedAt = elapsed
|
|
170
|
+
overlay.setAttribute('data-copy-transition', '')
|
|
171
|
+
}
|
|
172
|
+
pendingStage = name
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function advanceStageTransition(elapsed) {
|
|
176
|
+
if (!pendingStage || copyTransitionStartedAt === undefined) return
|
|
177
|
+
if (elapsed - copyTransitionStartedAt < schedule.copyFade) return
|
|
178
|
+
const next = pendingStage
|
|
179
|
+
pendingStage = undefined
|
|
180
|
+
copyTransitionStartedAt = undefined
|
|
181
|
+
applyStage(next)
|
|
182
|
+
overlay?.removeAttribute('data-copy-transition')
|
|
183
|
+
if (next === 'ready') readyStartedAt = elapsed
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stageForProgress(progress) {
|
|
187
|
+
if (progress >= 68) return 'visualOnline'
|
|
188
|
+
if (progress >= 32) return 'authenticating'
|
|
189
|
+
return 'linking'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function beginExit(elapsed, { failOpen = false } = {}) {
|
|
193
|
+
if (!overlay || exitStartedAt !== undefined) return
|
|
194
|
+
exitStartedAt = elapsed
|
|
195
|
+
overlay.toggleAttribute('data-fail-open', failOpen)
|
|
196
|
+
overlay.setAttribute('data-exit-content', '')
|
|
197
|
+
if (useReducedMotion) overlay.setAttribute('data-exiting', '')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function onVisibilityChange() {
|
|
201
|
+
if (document?.visibilityState === 'hidden') cleanup({ restoreFocus: false })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function reducedMotionRequested(forceReduced) {
|
|
205
|
+
if (typeof forceReduced === 'boolean') return forceReduced
|
|
206
|
+
return document?.documentElement?.dataset?.prtsMotion === 'reduced'
|
|
207
|
+
|| window?.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function lockSiblings() {
|
|
211
|
+
inertRecords = [...(document?.body?.children ?? [])]
|
|
212
|
+
.filter(node => node !== overlay)
|
|
213
|
+
.map(node => ({ node, hadAttribute: node.hasAttribute('inert'), inert: Boolean(node.inert) }))
|
|
214
|
+
for (const record of inertRecords) {
|
|
215
|
+
record.node.inert = true
|
|
216
|
+
record.node.setAttribute('inert', '')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function promoteToTopLayer() {
|
|
221
|
+
if (typeof overlay?.showPopover !== 'function') return
|
|
222
|
+
try {
|
|
223
|
+
overlay.showPopover()
|
|
224
|
+
} catch {
|
|
225
|
+
// A recognized but unavailable Popover API must not hide the fixed fallback.
|
|
226
|
+
overlay.removeAttribute('popover')
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function checkHostReady() {
|
|
231
|
+
if (hostReady) return true
|
|
232
|
+
if (!waitForReady) return (hostReady = true)
|
|
233
|
+
try {
|
|
234
|
+
if (readyWhen?.() === true) hostReady = true
|
|
235
|
+
} catch {
|
|
236
|
+
// A transient Host DOM read must not break the fail-open timeline.
|
|
237
|
+
}
|
|
238
|
+
return hostReady
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function tick(timestamp) {
|
|
242
|
+
if (!overlay) return
|
|
243
|
+
const time = Number.isFinite(timestamp) ? timestamp : now()
|
|
244
|
+
startedAt ??= time
|
|
245
|
+
const elapsed = Math.max(0, time - startedAt)
|
|
246
|
+
checkHostReady()
|
|
247
|
+
|
|
248
|
+
if (!overlay.hasAttribute('data-visible') && elapsed >= schedule.enter) {
|
|
249
|
+
overlay.setAttribute('data-visible', '')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (useReducedMotion) {
|
|
253
|
+
if (!hostReady && timeoutStartedAt === undefined && elapsed >= schedule.timeout) {
|
|
254
|
+
timeoutStartedAt = elapsed
|
|
255
|
+
}
|
|
256
|
+
if (timeoutStartedAt !== undefined && elapsed - timeoutStartedAt >= schedule.timeoutHold) {
|
|
257
|
+
beginExit(elapsed, { failOpen: true })
|
|
258
|
+
} else if (hostReady) {
|
|
259
|
+
beginExit(elapsed)
|
|
260
|
+
}
|
|
261
|
+
if (exitStartedAt !== undefined && elapsed - exitStartedAt >= schedule.reducedExit + schedule.completePadding) {
|
|
262
|
+
cleanup()
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
} else if (timeoutStartedAt !== undefined) {
|
|
266
|
+
updateProgress(90)
|
|
267
|
+
if (elapsed - timeoutStartedAt >= schedule.timeoutHold) beginExit(elapsed, { failOpen: true })
|
|
268
|
+
} else {
|
|
269
|
+
const approachProgress = 90 * easeOutCubic(elapsed / schedule.approach)
|
|
270
|
+
let progress = Math.min(90, approachProgress)
|
|
271
|
+
|
|
272
|
+
if (!hostReady && elapsed >= schedule.timeout) {
|
|
273
|
+
timeoutStartedAt = elapsed
|
|
274
|
+
} else if (hostReady && elapsed >= schedule.intro) {
|
|
275
|
+
if (finishStartedAt === undefined) {
|
|
276
|
+
finishStartedAt = elapsed
|
|
277
|
+
finishStartedProgress = progress
|
|
278
|
+
}
|
|
279
|
+
progress = finishStartedProgress
|
|
280
|
+
+ (100 - finishStartedProgress) * easeOutCubic((elapsed - finishStartedAt) / schedule.finish)
|
|
281
|
+
if (elapsed - finishStartedAt >= schedule.finish) {
|
|
282
|
+
progress = 100
|
|
283
|
+
requestStage('ready', elapsed)
|
|
284
|
+
} else {
|
|
285
|
+
requestStage(stageForProgress(progress), elapsed)
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
requestStage(stageForProgress(progress), elapsed)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
updateProgress(progress)
|
|
292
|
+
if (readyStartedAt !== undefined) {
|
|
293
|
+
const earliestExit = Math.max(
|
|
294
|
+
readyStartedAt + schedule.readyHold,
|
|
295
|
+
schedule.minimum - schedule.exit - schedule.completePadding,
|
|
296
|
+
)
|
|
297
|
+
if (elapsed >= earliestExit) beginExit(elapsed)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
advanceStageTransition(elapsed)
|
|
302
|
+
|
|
303
|
+
if (exitStartedAt !== undefined) {
|
|
304
|
+
if (!useReducedMotion && elapsed - exitStartedAt >= schedule.exitLead) {
|
|
305
|
+
overlay?.setAttribute('data-exiting', '')
|
|
306
|
+
}
|
|
307
|
+
const exitDuration = useReducedMotion ? schedule.reducedExit : schedule.exit
|
|
308
|
+
if (elapsed - exitStartedAt >= exitDuration + schedule.completePadding) {
|
|
309
|
+
cleanup()
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (elapsed >= schedule.watchdog) {
|
|
314
|
+
cleanup()
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
frame = requestFrame(tick)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function play({ reduced, waitForReady: shouldWait = false, readyWhen: isReady } = {}) {
|
|
321
|
+
if (overlay?.isConnected || !document?.body) return false
|
|
322
|
+
const template = document.createElement('template')
|
|
323
|
+
template.innerHTML = startupMarkup(prtsEmblem, rhodesEmblem).trim()
|
|
324
|
+
overlay = template.content.firstElementChild
|
|
325
|
+
if (!overlay) return false
|
|
326
|
+
|
|
327
|
+
startedAt = undefined
|
|
328
|
+
waitForReady = Boolean(shouldWait)
|
|
329
|
+
readyWhen = typeof isReady === 'function' ? isReady : undefined
|
|
330
|
+
hostReady = !waitForReady
|
|
331
|
+
finishStartedAt = undefined
|
|
332
|
+
readyStartedAt = undefined
|
|
333
|
+
exitStartedAt = undefined
|
|
334
|
+
timeoutStartedAt = undefined
|
|
335
|
+
currentStage = 'boot'
|
|
336
|
+
pendingStage = undefined
|
|
337
|
+
copyTransitionStartedAt = undefined
|
|
338
|
+
lastProgress = -1
|
|
339
|
+
useReducedMotion = reducedMotionRequested(reduced)
|
|
340
|
+
|
|
341
|
+
previousFocus = document.activeElement
|
|
342
|
+
document.body.appendChild(overlay)
|
|
343
|
+
promoteToTopLayer()
|
|
344
|
+
document.documentElement.setAttribute('data-prts-startup-active', '')
|
|
345
|
+
lockSiblings()
|
|
346
|
+
overlay.focus?.({ preventScroll: true })
|
|
347
|
+
document.addEventListener('visibilitychange', onVisibilityChange)
|
|
348
|
+
|
|
349
|
+
if (useReducedMotion) {
|
|
350
|
+
overlay.setAttribute('data-reduced-motion', '')
|
|
351
|
+
overlay.setAttribute('data-visible', '')
|
|
352
|
+
applyStage('visualOnline')
|
|
353
|
+
updateProgress(90)
|
|
354
|
+
}
|
|
355
|
+
frame = requestFrame(tick)
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
play,
|
|
361
|
+
ready() { hostReady = true },
|
|
362
|
+
stop: cleanup,
|
|
363
|
+
dispose: cleanup,
|
|
364
|
+
isActive: () => Boolean(overlay?.isConnected),
|
|
365
|
+
}
|
|
366
|
+
}
|