@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,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&nbsp;&nbsp;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
+ }