@ash-qw/dsh-theme-prts 2.5.2 → 2.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-qw/dsh-theme-prts",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web and the official desktop client",
@@ -11,6 +11,15 @@ function closestWithClassSuffix(node, suffix) {
11
11
  return null
12
12
  }
13
13
 
14
+ function isPendingInteractionAction(button) {
15
+ if (!button || button.tagName !== 'BUTTON') return false
16
+ if (button.closest('[data-approval-key]')) return true
17
+ if (button.closest('[data-question-key]')) {
18
+ return hasClassSuffix(button, '_waitButton') || Boolean(closestWithClassSuffix(button, '_footerActions'))
19
+ }
20
+ return Boolean(button.closest('[data-plan-review-key]') && closestWithClassSuffix(button, '_actions'))
21
+ }
22
+
14
23
  function classify(button, operation) {
15
24
  // The host's process disclosure owns its transparent, full-width hit area.
16
25
  if (button.hasAttribute('data-turn-process')) return null
@@ -138,14 +147,16 @@ function mountBodyTargetFrame(document, window, operation) {
138
147
  // Nonvisual hosts can still mount the theme without an animation clock.
139
148
  if (!window?.requestAnimationFrame || !window?.cancelAnimationFrame) return () => {}
140
149
  const composerButton=':is([data-composer-card],[data-prts-composer-fallback]) button:not([data-slot="conversation.input.attachments"] *,[data-prts-glass-menu] *,[data-prts-floating-glass] *,[role="menu"] *,[role="listbox"] *)'
141
- const selector='[data-prts-conversation-control],[data-presented-file],[data-changed-files],[data-produced-files-row] > button,[data-prts-glass-control],[data-prts-footer-action],[data-prts-transfer] button,'+composerButton
150
+ const selector='[data-prts-to-bottom],[data-prts-conversation-control],[data-presented-file],[data-changed-files],[data-produced-files-row] > button,[data-prts-glass-control],[data-prts-footer-action],[data-prts-transfer] button,'+composerButton
142
151
  const frame=document.createElement('div')
143
152
  frame.setAttribute('data-prts-body-target-frame','');frame.setAttribute('aria-hidden','true')
144
153
  const corners=Array.from({length:4},()=>frame.appendChild(document.createElement('i')))
145
154
  document.body.append(frame)
146
155
  if(frame.showPopover)frame.setAttribute('popover','manual')
147
- let layer=null
148
- const hideFrame=()=>{if(frame.hidePopover && frame.matches(':popover-open'))frame.hidePopover()}
156
+ let layer=null, imageRetiring=false
157
+ const hideFrame=()=>{
158
+ if(frame.hidePopover && frame.matches(':popover-open'))frame.hidePopover()
159
+ }
149
160
  const showFrame=()=>{
150
161
  if(!frame.showPopover)return
151
162
  const modal=target?.closest('dialog[open]')
@@ -153,27 +164,36 @@ function mountBodyTargetFrame(document, window, operation) {
153
164
  if(!frame.matches(':popover-open'))frame.showPopover()
154
165
  layer=modal
155
166
  }
156
- let target=null, hovered=null, focused=null, raf=0, pressed=false, disposed=false
157
- let pointer={x:0,y:0}, state=null, settled=false, tween=null, bounds=null, boundsDirty=false, hitDirty=false, exitGoal=null, motionDuration=240, targetArm=7
167
+ let target=null, hovered=null, focused=null, raf=0, pressed=false, releasePending=false, disposed=false
168
+ let pointer={x:0,y:0}, state=null, settled=false, tween=null, bounds=null, boundsDirty=false, hitDirty=false, exitGoal=null, motionDuration=240, targetArm=7, retiringBounds=null, targetRetires=false
158
169
  const resizeObserver=window.ResizeObserver?new window.ResizeObserver(()=>{boundsDirty=true;wake()}):null
159
170
  const reduced=window.matchMedia?.('(prefers-reduced-motion: reduce)')
160
171
  const fine=window.matchMedia?.('(hover:hover) and (pointer:fine)')
172
+ let activeModal=null
161
173
  const find=node=>{
174
+ if(activeModal && !activeModal.contains(node))return null
162
175
  if (!node?.closest || node.closest('[data-prts-workspace-actions],[data-prts-session-actions]')) return null
163
176
  const card=node.closest('[data-presented-file],[data-changed-files]')
164
- const candidate=card || node.closest(selector)
177
+ const button=node.closest('button')
178
+ const pendingAction=operation.contains(button) && isPendingInteractionAction(button) ? button : null
179
+ const candidate=card || pendingAction || node.closest(selector)
165
180
  return candidate && !candidate.matches(':disabled,[aria-disabled="true"]') && !candidate.closest('[inert]') ? candidate : null
166
181
  }
167
182
  const measure=()=>{bounds=target.getBoundingClientRect();targetArm=7+Math.max(0,Math.min(1,(Math.min(bounds.width,bounds.height)-36)/36))*5;boundsDirty=false}
168
183
  const wake=()=>{if(!raf && !disposed)raf=window.requestAnimationFrame(draw)}
169
184
  const choose=()=>{
170
- const next=hovered || focused
185
+ const next=(hovered?.isConnected?hovered:null) || (focused?.isConnected?focused:null)
171
186
  if(next!==target){
187
+ const retire=!next && target && !target.isConnected && targetRetires && releasePending && bounds && !reduced?.matches
188
+ imageRetiring=false
189
+ retiringBounds=retire?{left:bounds.left,top:bounds.top,width:bounds.width,height:bounds.height}:null
172
190
  target?.removeAttribute('data-prts-corner-target')
173
- pressed=false;settled=false;tween=null;exitGoal=null;motionDuration=240;target=next;boundsDirty=true
191
+ if(retire){hovered=focused=target=null;boundsDirty=false;resizeObserver?.disconnect();wake();return}
192
+ pressed=false;releasePending=false;settled=false;tween=null;exitGoal=null;motionDuration=240;target=next;boundsDirty=true
174
193
  resizeObserver?.disconnect()
175
194
  if(target)resizeObserver?.observe(target)
176
195
  if(target){
196
+ targetRetires=hasClassSuffix(target,'_toBottom') || isPendingInteractionAction(target)
177
197
  target.setAttribute('data-prts-corner-target','');showFrame()
178
198
  measure();const box=bounds
179
199
  if(!state || state.alpha<.01)state={x:(hovered?pointer.x:box.left+box.width/2)-10,y:(hovered?pointer.y:box.top+box.height/2)-10,w:20,h:20,alpha:0,arm:7}
@@ -185,14 +205,15 @@ function mountBodyTargetFrame(document, window, operation) {
185
205
  }
186
206
  function draw(time) {
187
207
  raf=0
208
+ if(target && !target.isConnected)choose()
188
209
  if(hitDirty){hitDirty=false;hovered=find(document.elementFromPoint(pointer.x,pointer.y));choose()}
189
- if(target && !target.isConnected){target.removeAttribute('data-prts-corner-target');target=hovered=focused=null;pressed=false;settled=false;exitGoal=null}
210
+ if(target && !target.isConnected){target.removeAttribute('data-prts-corner-target');target=hovered=focused=null;pressed=false;releasePending=false;settled=false;exitGoal=null}
190
211
  if(!state)return
191
212
  if(target && boundsDirty){
192
213
  measure()
193
- if(!bounds.width && !bounds.height){target.removeAttribute('data-prts-corner-target');target=hovered=focused=null;pressed=false;settled=false;exitGoal=null;resizeObserver?.disconnect()}
214
+ if(!bounds.width && !bounds.height){target.removeAttribute('data-prts-corner-target');target=hovered=focused=null;pressed=false;releasePending=false;settled=false;exitGoal=null;resizeObserver?.disconnect()}
194
215
  }
195
- const box=target?bounds:null,gap=pressed?3:5
216
+ const box=target?bounds:retiringBounds,gap=pressed?(imageRetiring?0:3):5
196
217
  // Freeze the exit destination: subsequent pointer motion must not restart the fade.
197
218
  const goal=box?{x:box.left-gap,y:box.top-gap,w:box.width+gap*2,h:box.height+gap*2,alpha:1,arm:targetArm}:
198
219
  (exitGoal ||= {x:pointer.x-10,y:pointer.y-10,w:20,h:20,alpha:0,arm:7})
@@ -223,7 +244,11 @@ function mountBodyTargetFrame(document, window, operation) {
223
244
  if(frame.style.opacity!==opacity)frame.style.opacity=opacity
224
245
  settled=!moving
225
246
  frame.toggleAttribute('data-pressed',pressed)
226
- if(moving)wake()
247
+ if(releasePending && pressed && !moving){
248
+ if(retiringBounds){exitGoal={...state,alpha:0};retiringBounds=null}
249
+ releasePending=false;pressed=false;motionDuration=imageRetiring?120:140;settled=false;wake()
250
+ }
251
+ else if(moving)wake()
227
252
  else {tween=null;if(!state.alpha)hideFrame()}
228
253
  }
229
254
  const move=event=>{
@@ -240,21 +265,49 @@ function mountBodyTargetFrame(document, window, operation) {
240
265
  pointer={x:event.clientX,y:event.clientY}
241
266
  if(event.pointerType==='touch')hovered=find(event.target)
242
267
  choose()
243
- if(target && find(event.target)===target){pressed=true;motionDuration=100;settled=false;wake()}
268
+ if(target && find(event.target)===target){pressed=true;releasePending=false;motionDuration=100;settled=false;wake()}
269
+ }
270
+ const release=()=>{
271
+ // A tap can release before the first paint. Finish one contraction, then
272
+ // expand; one flag keeps repeated taps interruptible without a timer queue.
273
+ if(pressed && !settled && !reduced?.matches){releasePending=true;settled=false;wake()}
274
+ else {pressed=false;releasePending=false;motionDuration=140;settled=false;wake()}
244
275
  }
245
- const up=event=>{pressed=false;motionDuration=140;settled=false;if(event.pointerType==='touch')hovered=null;choose()}
276
+ const up=event=>{release();if(event.pointerType==='touch')hovered=null;choose()}
246
277
  const reset=()=>{
247
278
  target?.removeAttribute('data-prts-corner-target');hideFrame()
248
- hovered=focused=target=null;state=tween=bounds=exitGoal=null
249
- pressed=false;settled=true;boundsDirty=hitDirty=false
279
+ hovered=focused=target=null;state=tween=bounds=exitGoal=retiringBounds=null
280
+ imageRetiring=false
281
+ pressed=false;releasePending=false;settled=true;boundsDirty=hitDirty=false
250
282
  resizeObserver?.disconnect();window.cancelAnimationFrame(raf);raf=0
251
283
  frame.style.opacity='0';frame.removeAttribute('data-pressed')
252
284
  }
285
+ const syncModal=()=>{
286
+ const modals=[...document.querySelectorAll('dialog[open],[role="dialog"][aria-modal="true"]')]
287
+ const previousModal=activeModal
288
+ activeModal=modals.filter(node=>!node.hidden && node.getAttribute('aria-hidden')!=='true').at(-1) ?? null
289
+ if(activeModal===previousModal)return
290
+ if(activeModal && target && !activeModal.contains(target) && target.querySelector('img') && activeModal.querySelector('img') && releasePending && bounds && !reduced?.matches){
291
+ // The preview is opaque: show a bounded click acknowledgment above it,
292
+ // then fade at the thumbnail position instead of retaining a hover target.
293
+ imageRetiring=true;motionDuration=120;tween=null
294
+ retiringBounds={left:bounds.left,top:bounds.top,width:bounds.width,height:bounds.height}
295
+ target.removeAttribute('data-prts-corner-target');hovered=focused=target=null
296
+ boundsDirty=hitDirty=false;resizeObserver?.disconnect();settled=false;wake()
297
+ }else if(imageRetiring || activeModal && (!target || !activeModal.contains(target)))reset()
298
+ }
299
+ // Native image previews mount a body portal without moving the pointer.
300
+ // Watch portal changes only, not the streaming conversation subtree.
301
+ const modalObserver=window.MutationObserver?new window.MutationObserver(syncModal):null
302
+ modalObserver?.observe(document.body,{childList:true})
303
+ const modalToggle=event=>{if(event.target?.matches?.('dialog'))syncModal()}
304
+ document.addEventListener('toggle',modalToggle,true)
305
+ syncModal()
253
306
  const visibility=()=>{if(document.hidden)reset()}
254
307
  const scroll=()=>{if(!target && !state?.alpha)return;boundsDirty=true;hitDirty=!!hovered;wake()}
255
308
  document.addEventListener('scroll',scroll,true)
256
309
  window.addEventListener('resize',reset)
257
- const key=event=>{if(target && (event.key===' ' || event.key==='Enter')){pressed=event.type==='keydown';motionDuration=pressed?100:140;settled=false;wake()}}
310
+ const key=event=>{if(target && (event.key===' ' || event.key==='Enter')){if(event.type==='keyup')release();else {pressed=true;releasePending=false;motionDuration=100;settled=false;wake()}}}
258
311
  const listeners=[
259
312
  [document,'pointermove',move],[document,'pointerleave',leave],
260
313
  [document,'focusin',focus],[document,'focusout',blur],
@@ -264,6 +317,7 @@ function mountBodyTargetFrame(document, window, operation) {
264
317
  ]
265
318
  for(const [node,event,handler] of listeners)node.addEventListener(event,handler)
266
319
  return ()=>{
320
+ modalObserver?.disconnect();document.removeEventListener('toggle',modalToggle,true)
267
321
  disposed=true;target?.removeAttribute('data-prts-corner-target');hideFrame();window.cancelAnimationFrame(raf);resizeObserver?.disconnect()
268
322
  document.removeEventListener('scroll',scroll,true);window.removeEventListener('resize',reset)
269
323
  for(const [node,event,handler] of listeners)node.removeEventListener(event,handler)
@@ -43,7 +43,7 @@ export function createAvatarStore({window,document}){
43
43
  const focus=()=>{if(window.fetch)void refresh().catch(()=>{})}
44
44
  window.addEventListener('focus',focus)
45
45
  return{
46
- snapshot:()=>state,refresh,
46
+ snapshot:()=>state,refresh,applySnapshot:apply,
47
47
  subscribe(fn){listeners.add(fn);fn(state);return()=>listeners.delete(fn)},
48
48
  async edit(role){
49
49
  const response=await window.fetch(API+'/edit/'+role,{credentials:'same-origin',signal:controller?.signal})
@@ -208,6 +208,7 @@ export function applyPrtsPlugin(ctx, environment) {
208
208
  document, window, assets, adapter, hostGeometry, customEmblemStore, avatarStore, backgroundStore,
209
209
  themeTransfer: themeTransfer ||= createThemeTransfer({ window, getPreferences: () => nativePreferences.value,
210
210
  savePreferences: (value, revision) => nativePreferences.replace(value, revision),
211
+ onAvatarConfiguration: value => avatarStore.applySnapshot(value),
211
212
  checkWritable: () => nativePreferences.checkWritable(), checkExport: () => nativePreferences.checkReadable(), themeVersion: version }),
212
213
  onSchemeToggle(next, interaction) {
213
214
  return theme.setTheme(next, { ...interaction, animate: true })
@@ -95,9 +95,13 @@ function visibleSessionSummaries(sessions, workspaces, sessionStatus) {
95
95
  })
96
96
  }
97
97
 
98
+ function setSidebarAttribute(node, attribute, value) {
99
+ if (node.getAttribute(attribute) !== value) node.setAttribute(attribute, value)
100
+ }
101
+
98
102
  function markSidebarNode(node, attribute, next) {
99
103
  if (!node) return
100
- node.setAttribute(attribute, '')
104
+ setSidebarAttribute(node, attribute, '')
101
105
  next.add(node)
102
106
  }
103
107
 
@@ -174,9 +178,9 @@ function ensureFacilityFace(document, row, kind, next) {
174
178
  face.setAttribute('data-prts-owned-facility', '')
175
179
  row.insertBefore(face, row.firstChild)
176
180
  }
177
- face.setAttribute('data-prts-facility-face', kind)
178
- face.setAttribute('data-prts-facility-texture', kind === 'workspace' ? 'silhouette' : 'pickup')
179
- face.setAttribute('aria-hidden', 'true')
181
+ setSidebarAttribute(face, 'data-prts-facility-face', kind)
182
+ setSidebarAttribute(face, 'data-prts-facility-texture', kind === 'workspace' ? 'silhouette' : 'pickup')
183
+ setSidebarAttribute(face, 'aria-hidden', 'true')
180
184
  next.add(face)
181
185
  return face
182
186
  }
@@ -188,8 +192,8 @@ function ensureFacilitySpine(document, row, kind, next) {
188
192
  spine.setAttribute('data-prts-owned-facility-spine', '')
189
193
  row.insertBefore(spine, row.firstChild)
190
194
  }
191
- spine.setAttribute('data-prts-facility-spine', kind)
192
- spine.setAttribute('aria-hidden', 'true')
195
+ setSidebarAttribute(spine, 'data-prts-facility-spine', kind)
196
+ setSidebarAttribute(spine, 'aria-hidden', 'true')
193
197
  next.add(spine)
194
198
  return spine
195
199
  }
@@ -223,6 +227,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
223
227
  let statusSourcesStarted = false
224
228
  let summaryRoot
225
229
  let summarySignature = ''
230
+ let sourceSummarySignature = ''
226
231
  let heldPreviewState
227
232
  let hoveredSummary, summaryLeaveTimer
228
233
  let summaryDismissed = false, pointerLeftSummary = false
@@ -327,18 +332,26 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
327
332
  sourceRetryTimer = undefined
328
333
  }
329
334
  }
335
+ function onSummarySourceChange() {
336
+ // Streaming activity often republishes unchanged title/status summaries.
337
+ // Native DOM mutations still handle row mounts, selection and hierarchy.
338
+ const signature = JSON.stringify(visibleSessionSummaries(sessions, workspaces, sessionStatus))
339
+ if (signature === sourceSummarySignature && !pendingReveal) return
340
+ sourceSummarySignature = signature
341
+ queueScan()
342
+ }
330
343
  function connectSummarySources() {
331
344
  if (getSessions) sessions = getSessions()
332
345
  if (getWorkspaces) workspaces = getWorkspaces()
333
346
  if (subscribedSessions !== sessions?.list) {
334
347
  stopSessionList()
335
348
  subscribedSessions = sessions?.list
336
- stopSessionList = subscribedSessions?.subscribe?.(queueScan) ?? (() => {})
349
+ stopSessionList = subscribedSessions?.subscribe?.(onSummarySourceChange) ?? (() => {})
337
350
  }
338
351
  if (subscribedWorkspaces !== workspaces?.list) {
339
352
  stopWorkspaceList()
340
353
  subscribedWorkspaces = workspaces?.list
341
- stopWorkspaceList = subscribedWorkspaces?.subscribe?.(queueScan) ?? (() => {})
354
+ stopWorkspaceList = subscribedWorkspaces?.subscribe?.(onSummarySourceChange) ?? (() => {})
342
355
  }
343
356
  }
344
357
  function connectSessionStatus() {
@@ -346,7 +359,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
346
359
  if (source === sessionStatus) return
347
360
  stopSessionStatus()
348
361
  sessionStatus = source
349
- stopSessionStatus = source?.subscribe?.(queueScan) ?? (() => {})
362
+ stopSessionStatus = source?.subscribe?.(onSummarySourceChange) ?? (() => {})
350
363
  }
351
364
 
352
365
  let selectionTarget, selectionTargetTimer
@@ -861,6 +874,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
861
874
  function scan() {
862
875
  if (!adapterActive) return
863
876
  syncSummarySources()
877
+ sourceSummarySignature = JSON.stringify(visibleSessionSummaries(sessions, workspaces, sessionStatus))
864
878
  const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
865
879
  const next = new Set()
866
880
  const nextVectorOwners = new Set()
@@ -919,16 +933,16 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
919
933
  }
920
934
  markSidebarNode(row, 'data-prts-session-row', next)
921
935
  if (state) {
922
- row.setAttribute('data-prts-session-state', state)
936
+ setSidebarAttribute(row, 'data-prts-session-state', state)
923
937
  next.add(row)
924
938
  } else {
925
939
  row.removeAttribute('data-prts-session-state')
926
940
  }
927
- row.dataset.prtsSessionIndex = sessionIndex
941
+ setSidebarAttribute(row, 'data-prts-session-index', sessionIndex)
928
942
  const title = resolveRowTitle(row, 'session', controls.actions)
929
943
  markSidebarNode(title, 'data-prts-row-title', next)
930
944
  markSidebarNode(resolveSessionTime(row, title, controls.actions), 'data-prts-session-time', next)
931
- if (title) title.dataset.prtsSessionIndex = sessionIndex
945
+ if (title) setSidebarAttribute(title, 'data-prts-session-index', sessionIndex)
932
946
  row.toggleAttribute('data-prts-row-menu-open', controls.menu?.getAttribute('aria-expanded') === 'true'
933
947
  || row.classList.contains('menuOpen'))
934
948
  markSidebarNode(controls.actions, 'data-prts-session-actions', next)
@@ -101,7 +101,7 @@ export function describeThemePackage(value) {
101
101
  return `${count} 项主题设置、${transferSlots.filter(k => a.backgrounds[k]).length} 张背景、${transferRoles.filter(k => a.avatars[k]).length} 个头像、${a.emblems.items.length} 个自定义徽记。将替换当前素材及徽记排列,空项恢复为默认。`
102
102
  }
103
103
 
104
- export function createThemeTransfer({ window, getPreferences, savePreferences, checkWritable, checkExport = checkWritable, themeVersion }) {
104
+ export function createThemeTransfer({ window, getPreferences, savePreferences, checkWritable, checkExport = checkWritable, themeVersion, onAvatarConfiguration = () => {} }) {
105
105
  let busy = false, backup
106
106
  const request = async (path, data, signal) => {
107
107
  checkTransferCancelled(signal)
@@ -198,7 +198,10 @@ export function createThemeTransfer({ window, getPreferences, savePreferences, c
198
198
  undo.push(async () => {
199
199
  const restored = await request(kind + '/configuration', { ...previous, revision: revisions[kind] })
200
200
  revisions[kind] = restored.revision
201
+ if (kind === 'avatars') onAvatarConfiguration(restored)
201
202
  })
203
+ // Apply the acknowledged images locally; SSE remains for other pages.
204
+ if (kind === 'avatars') onAvatarConfiguration(result)
202
205
  }
203
206
  try {
204
207
  if (value.assets) {
@@ -485,3 +485,13 @@ background:color-mix(in srgb,var(--prts-panel) 28%,transparent);
485
485
  -webkit-backdrop-filter:blur(12px);
486
486
  backdrop-filter:blur(12px);
487
487
  }
488
+
489
+ /* The task manager mounts directly in main, outside the conversation root. */
490
+ html[data-dsh-prts] [data-prts-region="operation"] [data-testid="task-manager-page"],
491
+ html[data-dsh-prts] [data-testid="task-manager-page"] :is([class$="_detail"],[class$="_detailNotice"]) {
492
+ background: var(--prts-canvas);
493
+ }
494
+ html[data-dsh-prts]:is([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"][data-prts-background-conversation]) [data-prts-region="operation"] [data-testid="task-manager-page"],
495
+ html[data-dsh-prts]:is([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"][data-prts-background-conversation]) [data-testid="task-manager-page"] :is([class$="_detail"],[class$="_detailNotice"]) {
496
+ background: transparent;
497
+ }