@ash-qw/dsh-theme-prts 0.1.96 → 0.1.98

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.
@@ -24,7 +24,7 @@ const STARTUP_STAGES = Object.freeze({
24
24
 
25
25
  function startupMarkup(prtsEmblem = '', rhodesEmblem = '') {
26
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">
27
+ return `<section data-prts-startup data-stage="boot" popover="manual" role="status" aria-live="assertive" aria-label="P.R.T.S. 启动序列">
28
28
  <div data-prts-startup-grid aria-hidden="true"></div>
29
29
  <div data-prts-startup-scan aria-hidden="true"></div>
30
30
  <div data-prts-startup-cut="left" aria-hidden="true"></div>
@@ -73,16 +73,60 @@ function easeOutCubic(value) {
73
73
  return 1 - Math.pow(1 - clamp01(value), 3)
74
74
  }
75
75
 
76
+ function approachProgressAt(elapsed, duration) {
77
+ if (!(duration > 0)) return 90
78
+ return 90 * easeOutCubic(elapsed / duration)
79
+ }
80
+
81
+ function approachVelocityAt(elapsed, duration) {
82
+ if (!(duration > 0)) return 0
83
+ const remaining = 1 - clamp01(elapsed / duration)
84
+ return 270 * remaining * remaining / duration
85
+ }
86
+
87
+ function finishProgressAt(startProgress, startVelocity, elapsed, duration) {
88
+ if (!(duration > 0)) return 100
89
+ const progressDelta = Math.max(0, 100 - startProgress)
90
+ if (progressDelta === 0) return 100
91
+ const time = clamp01(elapsed / duration)
92
+ const timeSquared = time * time
93
+ const timeCubed = timeSquared * time
94
+ const startWeight = 2 * timeCubed - 3 * timeSquared + 1
95
+ const velocityWeight = timeCubed - 2 * timeSquared + time
96
+ const endWeight = -2 * timeCubed + 3 * timeSquared
97
+ const startTangent = Math.min(progressDelta * 3, Math.max(0, startVelocity) * duration)
98
+ const progress = startWeight * startProgress
99
+ + velocityWeight * startTangent
100
+ + endWeight * 100
101
+ return Math.max(startProgress, Math.min(100, progress))
102
+ }
103
+
76
104
  export function findNativeHarnessLoader(document) {
77
105
  return document?.querySelector?.('[data-dsh-boot]') ?? null
78
106
  }
79
107
 
108
+ function matchesOpenTopLayer(node) {
109
+ for (const selector of [':modal', ':popover-open']) {
110
+ try {
111
+ if (node?.matches?.(selector)) return true
112
+ } catch {}
113
+ }
114
+ return false
115
+ }
116
+
117
+ function hostHasOpenTopLayer(document, overlay) {
118
+ const candidates = [...(document?.querySelectorAll?.('dialog[open], [popover]') ?? [])]
119
+ return candidates.some(node => node !== overlay && matchesOpenTopLayer(node))
120
+ }
121
+
80
122
  export function createPrtsStartupSequence({
81
123
  document,
82
124
  window,
83
125
  prtsEmblem = '',
84
126
  rhodesEmblem = '',
85
127
  timings = PRTS_STARTUP_TIMINGS,
128
+ onActiveChange = () => {},
129
+ onReady = () => {},
86
130
  } = {}) {
87
131
  const schedule = { ...PRTS_STARTUP_TIMINGS, ...timings }
88
132
  let overlay
@@ -95,6 +139,7 @@ export function createPrtsStartupSequence({
95
139
  let hostReady = false
96
140
  let finishStartedAt
97
141
  let finishStartedProgress = 0
142
+ let finishStartedVelocity = 0
98
143
  let readyStartedAt
99
144
  let exitStartedAt
100
145
  let timeoutStartedAt
@@ -102,7 +147,12 @@ export function createPrtsStartupSequence({
102
147
  let pendingStage
103
148
  let copyTransitionStartedAt
104
149
  let lastProgress = -1
150
+ let lastRoundedProgress = -1
105
151
  let useReducedMotion = false
152
+ let progressFill
153
+ let progressPercent
154
+ let activeReported = false
155
+ let readyReported = false
106
156
 
107
157
  const now = () => window?.performance?.now?.() ?? Date.now()
108
158
  const requestFrame = callback => window?.requestAnimationFrame?.(callback)
@@ -113,6 +163,21 @@ export function createPrtsStartupSequence({
113
163
  else window?.clearTimeout?.(id)
114
164
  }
115
165
 
166
+ function notify(callback, value) {
167
+ try {
168
+ callback(value)
169
+ } catch {
170
+ // Startup remains fail-open when an integration callback fails.
171
+ }
172
+ }
173
+
174
+ function reportActive(active) {
175
+ const next = Boolean(active)
176
+ if (activeReported === next) return
177
+ activeReported = next
178
+ notify(onActiveChange, next)
179
+ }
180
+
116
181
  function restoreInteractivity() {
117
182
  for (const record of inertRecords) {
118
183
  if (!record.node?.isConnected) continue
@@ -135,9 +200,12 @@ export function createPrtsStartupSequence({
135
200
  }
136
201
  overlay?.remove?.()
137
202
  overlay = undefined
203
+ progressFill = undefined
204
+ progressPercent = undefined
138
205
  restoreInteractivity()
139
206
  if (restoreFocus && previousFocus?.isConnected) previousFocus.focus?.()
140
207
  previousFocus = undefined
208
+ reportActive(false)
141
209
  }
142
210
 
143
211
  function updateProgress(progress) {
@@ -145,9 +213,12 @@ export function createPrtsStartupSequence({
145
213
  const safeProgress = Math.max(0, Math.min(100, progress))
146
214
  if (Math.abs(safeProgress - lastProgress) < .01) return
147
215
  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')}%`
216
+ if (progressFill) progressFill.style.transform = `scaleX(${(safeProgress / 100).toFixed(4)})`
217
+ const roundedProgress = Math.round(safeProgress)
218
+ if (progressPercent && roundedProgress !== lastRoundedProgress) {
219
+ lastRoundedProgress = roundedProgress
220
+ progressPercent.textContent = `${String(roundedProgress).padStart(3, '0')}%`
221
+ }
151
222
  }
152
223
 
153
224
  function applyStage(name) {
@@ -161,6 +232,10 @@ export function createPrtsStartupSequence({
161
232
  if (label) label.textContent = stage.label
162
233
  if (detail) detail.textContent = stage.detail
163
234
  overlay.setAttribute('aria-label', `P.R.T.S. 启动序列:${stage.detail}`)
235
+ if (name === 'ready' && !readyReported) {
236
+ readyReported = true
237
+ notify(onReady)
238
+ }
164
239
  }
165
240
 
166
241
  function requestStage(name, elapsed) {
@@ -218,7 +293,14 @@ export function createPrtsStartupSequence({
218
293
  }
219
294
 
220
295
  function promoteToTopLayer() {
221
- if (typeof overlay?.showPopover !== 'function') return
296
+ if (!hostHasOpenTopLayer(document, overlay)) {
297
+ overlay?.removeAttribute('popover')
298
+ return
299
+ }
300
+ if (typeof overlay?.showPopover !== 'function') {
301
+ overlay?.removeAttribute('popover')
302
+ return
303
+ }
222
304
  try {
223
305
  overlay.showPopover()
224
306
  } catch {
@@ -266,7 +348,7 @@ export function createPrtsStartupSequence({
266
348
  updateProgress(90)
267
349
  if (elapsed - timeoutStartedAt >= schedule.timeoutHold) beginExit(elapsed, { failOpen: true })
268
350
  } else {
269
- const approachProgress = 90 * easeOutCubic(elapsed / schedule.approach)
351
+ const approachProgress = approachProgressAt(elapsed, schedule.approach)
270
352
  let progress = Math.min(90, approachProgress)
271
353
 
272
354
  if (!hostReady && elapsed >= schedule.timeout) {
@@ -275,9 +357,14 @@ export function createPrtsStartupSequence({
275
357
  if (finishStartedAt === undefined) {
276
358
  finishStartedAt = elapsed
277
359
  finishStartedProgress = progress
360
+ finishStartedVelocity = approachVelocityAt(elapsed, schedule.approach)
278
361
  }
279
- progress = finishStartedProgress
280
- + (100 - finishStartedProgress) * easeOutCubic((elapsed - finishStartedAt) / schedule.finish)
362
+ progress = finishProgressAt(
363
+ finishStartedProgress,
364
+ finishStartedVelocity,
365
+ elapsed - finishStartedAt,
366
+ schedule.finish,
367
+ )
281
368
  if (elapsed - finishStartedAt >= schedule.finish) {
282
369
  progress = 100
283
370
  requestStage('ready', elapsed)
@@ -332,18 +419,24 @@ export function createPrtsStartupSequence({
332
419
  readyStartedAt = undefined
333
420
  exitStartedAt = undefined
334
421
  timeoutStartedAt = undefined
422
+ finishStartedVelocity = 0
335
423
  currentStage = 'boot'
336
424
  pendingStage = undefined
337
425
  copyTransitionStartedAt = undefined
338
426
  lastProgress = -1
427
+ activeReported = false
428
+ readyReported = false
339
429
  useReducedMotion = reducedMotionRequested(reduced)
340
430
 
341
431
  previousFocus = document.activeElement
432
+ lastRoundedProgress = -1
342
433
  document.body.appendChild(overlay)
434
+ progressFill = overlay.querySelector('[data-prts-startup-fill]')
435
+ progressPercent = overlay.querySelector('[data-prts-startup-percent]')
343
436
  promoteToTopLayer()
344
437
  document.documentElement.setAttribute('data-prts-startup-active', '')
345
438
  lockSiblings()
346
- overlay.focus?.({ preventScroll: true })
439
+ reportActive(true)
347
440
  document.addEventListener('visibilitychange', onVisibilityChange)
348
441
 
349
442
  if (useReducedMotion) {
@@ -7,6 +7,7 @@ const ROOT_ATTRIBUTES = [
7
7
  'data-prts-glass',
8
8
  'data-prts-motion',
9
9
  'data-prts-particle-pattern',
10
+ 'data-prts-conversation-style',
10
11
  ]
11
12
 
12
13
  const TRANSITION_ATTRIBUTE = 'data-prts-scheme-transition'
@@ -715,6 +716,7 @@ export function createThemeController({ document, window, cssText, service, onTr
715
716
  root.dataset.prtsGlass = preferences.glass
716
717
  root.dataset.prtsMotion = preferences.motion
717
718
  root.dataset.prtsParticlePattern = 'orthogonal'
719
+ root.dataset.prtsConversationStyle = preferences.conversationStyle
718
720
  refresh()
719
721
  },
720
722
  sync,
@@ -7,6 +7,7 @@ const ROOT_ATTRIBUTES = [
7
7
  'data-prts-glass',
8
8
  'data-prts-motion',
9
9
  'data-prts-particle-pattern',
10
+ 'data-prts-conversation-style',
10
11
  ]
11
12
 
12
13
  function themeIdentity(value) {
@@ -108,6 +109,7 @@ export function createThemeController({ document, window, cssText, service }) {
108
109
  root.dataset.prtsGlass = preferences.glass
109
110
  root.dataset.prtsMotion = preferences.motion
110
111
  root.dataset.prtsParticlePattern = 'orthogonal'
112
+ root.dataset.prtsConversationStyle = preferences.conversationStyle
111
113
  refresh()
112
114
  },
113
115
  sync,
@@ -19,6 +19,7 @@ const COMMON_CONTROLS = [
19
19
  ['texture', '环境底纹', [['off', '关闭'], ['restrained', '克制'], ['full', '完整']]],
20
20
  ['glass', '玻璃材质', [['off', '关闭'], ['soft', '柔和'], ['standard', '标准'], ['clear', '清晰']]],
21
21
  ['motion', '动态效果', [['system', '跟随系统'], ['reduced', '减少']]],
22
+ ['conversationStyle', '会话主题', [['native', '原有'], ['deck-chat', '通讯链路']]],
22
23
  ]
23
24
 
24
25
  const PARTICLE_OPTIONS = [
@@ -34,6 +35,8 @@ function settingButtons(key, label, options) {
34
35
  ? '<small data-prts-setting-note data-prts-effective-motion hidden>系统当前要求减少动态</small>'
35
36
  : key === 'railDefaultHidden'
36
37
  ? '<small data-prts-setting-note>在任意窗口宽度下默认收起 P.R.T.S. 导航,可从左侧按钮临时展开</small>'
38
+ : key === 'conversationStyle'
39
+ ? '<small data-prts-setting-note>立即切换会话背景、头像与消息气泡</small>'
37
40
  : ''
38
41
  const sample = value => key === 'texture' || key === 'glass'
39
42
  ? `<i data-prts-option-sample="${key}" data-prts-option-sample-value="${value}" aria-hidden="true"></i>`