@ash-qw/dsh-theme-prts 2.6.0 → 2.7.0

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.6.0",
3
+ "version": "2.7.0",
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",
@@ -69,13 +69,15 @@
69
69
  },
70
70
  "engines": {
71
71
  "node": ">=20.9.0",
72
- "dsh": "0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.5"
72
+ "dsh": "0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.5 || 0.2.2-rc.1"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@playwright/test": "1.55.1",
76
76
  "esbuild": "^0.28.2",
77
+ "gsap": "3.14.2",
77
78
  "jsdom": "24.1.3",
78
- "markdown-it": "^14.3.3"
79
+ "markdown-it": "^14.3.3",
80
+ "mouse-follower": "1.2.1"
79
81
  },
80
82
  "dependencies": {
81
83
  "@deepseek-ai/schemastery": "~3.18.4",
@@ -9,11 +9,19 @@ export function mountAvatarCrop({panel,window,load,onSave}){
9
9
  <div data-crop-tools><label>缩放 <input type="range" min="1" max="4" step=".01" value="1" aria-label="头像裁剪缩放"></label><button type="button" data-crop-center>重置裁剪</button></div>
10
10
  <p role="status" data-prts-avatar-crop-status></p>
11
11
  <footer><button type="button" data-crop-reset>恢复默认</button><span></span><button type="button" data-crop-cancel>取消</button><button type="button" data-crop-save>保存头像</button></footer>`
12
+ const confirmation=panel.ownerDocument.createElement('dialog')
13
+ confirmation.className='prts-page-confirm'
14
+ confirmation.setAttribute('aria-labelledby','prts-avatar-reset-title')
15
+ confirmation.innerHTML='<strong id="prts-avatar-reset-title">恢复默认头像?</strong><p>当前头像图片及裁剪设置将被移除。</p><footer><button type="button" data-keep>保留头像</button><button type="button" data-confirm>恢复默认</button></footer>'
16
+ dialog.append(confirmation)
17
+ confirmation.querySelector('[data-keep]').onclick=()=>confirmation.close()
18
+ confirmation.addEventListener('cancel',e=>{e.stopPropagation()})
19
+ confirmation.addEventListener('keydown',e=>{if(e.key==='Escape')e.stopPropagation()})
12
20
  panel.append(dialog)
13
21
  const canvas=dialog.querySelector('canvas'),zoom=dialog.querySelector('input[type=range]'),status=dialog.querySelector('[role=status]')
14
22
  let bitmap,original,role,crop={zoom:1,x:.5,y:.5},drag,busy=false,disposed=false,generation=0,previousFocus,revision,hasCustom=false,ready=false
15
23
  const clean=()=>{bitmap?.close();bitmap=null;original=null;drag=null}
16
- const close=()=>{generation++;dialog.close?.();clean();previousFocus?.focus?.({preventScroll:true})}
24
+ const close=()=>{generation++;confirmation.close?.();dialog.close?.();clean();previousFocus?.focus?.({preventScroll:true})}
17
25
  const draw=()=>{
18
26
  zoom.value=String(crop.zoom)
19
27
  if(!bitmap){canvas.getContext('2d').clearRect(0,0,320,320);return}
@@ -103,8 +111,13 @@ export function mountAvatarCrop({panel,window,load,onSave}){
103
111
  catch{status.textContent='图片无法读取,请选择不超过 5 MB、2500 万像素的图片'}
104
112
  finally{busy=false;controls()}
105
113
  })
106
- dialog.querySelector('[data-crop-reset]').addEventListener('click',async()=>{
107
- if(busy||!hasCustom||!window.confirm('恢复默认头像将移除当前图片及裁剪设置,是否继续?'))return
114
+ dialog.querySelector('[data-crop-reset]').addEventListener('click',()=>{
115
+ if(busy||!hasCustom)return
116
+ confirmation.showModal();confirmation.querySelector('[data-keep]').focus()
117
+ })
118
+ confirmation.querySelector('[data-confirm]').addEventListener('click',async()=>{
119
+ if(busy||!hasCustom)return
120
+ confirmation.close()
108
121
  busy=true;controls()
109
122
  try{await onSave(role,null,undefined,revision);if(!disposed)close()}
110
123
  catch(error){status.textContent=error.message}
@@ -76,7 +76,8 @@ export function mountBackgroundRegionEditor({section,window,store}) {
76
76
  event.preventDefault();const bounds=stage.getBoundingClientRect();if(!bounds.width||!bounds.height)return
77
77
  const x=clamp((event.clientX-bounds.left)/bounds.width,0,1),y=clamp((event.clientY-bounds.top)/bounds.height,0,1)
78
78
  drag={x,y,bounds,initial:{...draft},corner:event.target.dataset.prtsRegionHandle,id:event.pointerId}
79
- stage.style.cursor=window.getComputedStyle(event.target).cursor
79
+ const cursorStyle=window.getComputedStyle(event.target)
80
+ stage.style.cursor=cursorStyle.getPropertyValue('--prts-source-cursor').trim() || cursorStyle.cursor
80
81
  stage.setPointerCapture?.(event.pointerId);draw()
81
82
  }
82
83
  function adjust(initial,dx,dy,corner){
@@ -146,7 +146,7 @@ export function createConversationControlAdapter({ document, window }) {
146
146
  }
147
147
  }
148
148
 
149
- // One shared frame follows body targets, including transitions between neighboring controls.
149
+ // One shared frame for card transitions and nearby compact controls.
150
150
  function mountBodyTargetFrame(document, window, operation) {
151
151
  // Nonvisual hosts can still mount the theme without an animation clock.
152
152
  if (!window?.requestAnimationFrame || !window?.cancelAnimationFrame) return () => {}
@@ -157,9 +157,46 @@ function mountBodyTargetFrame(document, window, operation) {
157
157
  const corners=Array.from({length:4},()=>frame.appendChild(document.createElement('i')))
158
158
  document.body.append(frame)
159
159
  if(frame.showPopover)frame.setAttribute('popover','manual')
160
+ let ghost=null, ghostCorners=[], ghostAnimations=[]
161
+ const clearGhost=()=>{
162
+ for(const animation of ghostAnimations)animation.cancel()
163
+ if(ghost)ghost.style.opacity='0'
164
+ }
165
+ const retireGhost=()=>{
166
+ // Allocate one reusable layer and five animations per adapter lifetime.
167
+ // Repeated handoffs must not churn body portals or their observers.
168
+ if(!ghost){
169
+ ghost=frame.cloneNode(true)
170
+ ghost.removeAttribute('data-prts-body-target-frame')
171
+ ghost.setAttribute('data-prts-body-target-ghost','')
172
+ ghost.removeAttribute('data-pressed')
173
+ ghost.style.opacity='0'
174
+ ghostCorners=[...ghost.children]
175
+ document.body.append(ghost)
176
+ ghostAnimations=[...ghostCorners,ghost].map(node=>{
177
+ const animation=node.animate([],{duration:80,easing:'ease-out',fill:'forwards'})
178
+ animation.cancel()
179
+ return animation
180
+ })
181
+ ghostAnimations[4].onfinish=()=>{
182
+ if(ghostAnimations[4]?.playState==='finished')clearGhost()
183
+ }
184
+ }
185
+ ghost.style.color=frame.style.color
186
+ ghost.style.setProperty('--prts-arm-scale',frame.style.getPropertyValue('--prts-arm-scale'))
187
+ if(ghost.showPopover && !ghost.matches(':popover-open'))ghost.showPopover()
188
+ for(const [index,corner] of corners.entries()){
189
+ const from=corner.style.transform
190
+ const dx=index%2?-4:4,dy=index<2?4:-4
191
+ ghostAnimations[index].effect.setKeyframes([{transform:from},{transform:from+' translate('+dx+'px,'+dy+'px)'}])
192
+ }
193
+ ghostAnimations[4].effect.setKeyframes([{opacity:frame.style.opacity},{opacity:0}])
194
+ for(const animation of ghostAnimations){animation.currentTime=0;animation.play()}
195
+ }
160
196
  let layer=null, imageRetiring=false
161
197
  const hideFrame=()=>{
162
198
  if(frame.hidePopover && frame.matches(':popover-open'))frame.hidePopover()
199
+ if(ghost?.hidePopover && ghost.matches(':popover-open'))ghost.hidePopover()
163
200
  }
164
201
  const showFrame=()=>{
165
202
  if(!frame.showPopover)return
@@ -171,6 +208,8 @@ function mountBodyTargetFrame(document, window, operation) {
171
208
  let target=null, hovered=null, focused=null, raf=0, pressed=false, releasePending=false, disposed=false
172
209
  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
173
210
  let moveRaf=0, pendingMove=null
211
+ let spring=false, springTime=null, springStart=null, velocity={}
212
+ let departed=null
174
213
  const resizeObserver=window.ResizeObserver?new window.ResizeObserver(entries=>{
175
214
  if(!target)return
176
215
  const entry=entries.find(item=>item.target===target),size=entry?.borderBoxSize?.[0]
@@ -182,10 +221,11 @@ function mountBodyTargetFrame(document, window, operation) {
182
221
  const reduced=window.matchMedia?.('(prefers-reduced-motion: reduce)')
183
222
  const fine=window.matchMedia?.('(hover:hover) and (pointer:fine)')
184
223
  let activeModal=null
224
+ const cardSelector='[data-presented-file],[data-changed-files],[data-tool="schedule_create"]'
185
225
  const find=node=>{
186
226
  if(activeModal && !activeModal.contains(node))return null
187
227
  if (!node?.closest || node.closest('[data-prts-workspace-actions],[data-prts-session-actions]')) return null
188
- const container=node.closest('[data-presented-file],[data-changed-files],[data-tool="schedule_create"]')
228
+ const container=node.closest(cardSelector)
189
229
  const button=node.closest('button')
190
230
  let cardTarget=null
191
231
  if(container){
@@ -209,20 +249,55 @@ function mountBodyTargetFrame(document, window, operation) {
209
249
  const next=(hovered?.isConnected?hovered:null) || (focused?.isConnected?focused:null)
210
250
  if(next!==target){
211
251
  const nextBounds=next?.getBoundingClientRect()
252
+ const now=window.performance.now()
253
+ const previous=target || (departed && now-departed.time<=100?departed.node:null)
254
+ const previousBounds=target?bounds:departed?.bounds
255
+ const card=previous?.closest(cardSelector)
256
+ const sameCard=Boolean(target && card && card===next?.closest(cardSelector))
257
+ const compact=(node,box)=>node?.matches('button,a[href],[role="button"],[role="link"]')
258
+ && box?.width>0 && box.width<=160 && box.height>0 && box.height<=64
259
+ const nearby=Boolean(next && previous?.isConnected && !card && !next.closest(cardSelector)
260
+ && compact(previous,previousBounds) && compact(next,nextBounds)
261
+ && previous.closest('dialog,[role="dialog"]')===next.closest('dialog,[role="dialog"]'))
262
+ const eligible=Boolean(next && previous?.isConnected && (sameCard || nearby)
263
+ && state?.alpha>0 && !reduced?.matches)
264
+ // Maximum corner travel catches wide-card -> edge-button transitions,
265
+ // even when the two centers happen to be close.
266
+ const travel=eligible?Math.max(
267
+ Math.hypot(state.x-(nextBounds.left-5),state.y-(nextBounds.top-5)),
268
+ Math.hypot(state.x+state.w-(nextBounds.right+5),state.y-(nextBounds.top-5)),
269
+ Math.hypot(state.x-(nextBounds.left-5),state.y+state.h-(nextBounds.bottom+5)),
270
+ Math.hypot(state.x+state.w-(nextBounds.right+5),state.y+state.h-(nextBounds.bottom+5))):0
271
+ const scale=eligible?Math.max(state.w/(nextBounds.width+10),(nextBounds.width+10)/state.w,state.h/(nextBounds.height+10),(nextBounds.height+10)/state.h):1
272
+ const morph=eligible && (sameCard || travel<=96 && scale<=2)
273
+ const handoff=morph && (travel>96 || scale>2)
274
+ if(!next && target)departed={node:target,bounds,time:now}
275
+ else if(next)departed=null
276
+ clearGhost()
277
+ if(handoff && frame.animate)retireGhost()
278
+ spring=morph && !handoff;springStart=null
279
+ if(!spring){springTime=null;velocity={}}
212
280
  const retire=!next && target && !target.isConnected && targetRetires && releasePending && bounds && !reduced?.matches
213
281
  imageRetiring=false
214
282
  retiringBounds=retire?{left:bounds.left,top:bounds.top,width:bounds.width,height:bounds.height}:null
215
283
  target?.removeAttribute('data-prts-corner-target')
216
284
  if(retire){hovered=focused=target=null;boundsDirty=false;resizeObserver?.disconnect();wake();return}
217
- pressed=false;releasePending=false;settled=false;tween=null;exitGoal=null;motionDuration=240;target=next;boundsDirty=true
285
+ pressed=false;releasePending=false;settled=false;tween=null;exitGoal=null;motionDuration=morph?120:240;target=next;boundsDirty=true
218
286
  resizeObserver?.disconnect()
219
287
  if(target)resizeObserver?.observe(target)
220
288
  if(target){
221
289
  targetRetires=hasClassSuffix(target,'_toBottom') || isPendingInteractionAction(target)
222
290
  target.setAttribute('data-prts-corner-target','');showFrame()
223
291
  measure(nextBounds);const box=bounds
224
- 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}
225
- frame.style.opacity='0'
292
+ // Preserve the last painted geometry when interrupted: no restart from
293
+ // the pointer, opacity flash, queued animations, or extra layout reads.
294
+ if(handoff){
295
+ state={x:box.left-12,y:box.top-12,w:box.width+24,h:box.height+24,alpha:0,arm:targetArm}
296
+ frame.style.opacity='0'
297
+ }else if(!morph){
298
+ 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}
299
+ frame.style.opacity='0'
300
+ }
226
301
  frame.style.color=target.getAttribute('data-prts-conversation-control')==='danger'?window.getComputedStyle(target).getPropertyValue('--prts-red'):'var(--prts-corner-color, var(--prts-yellow))'
227
302
  }
228
303
  }
@@ -248,15 +323,40 @@ function mountBodyTargetFrame(document, window, operation) {
248
323
  const edgeRight=pixel(goal.x+goal.w),edgeBottom=pixel(goal.y+goal.h)
249
324
  goal.x=pixel(goal.x);goal.y=pixel(goal.y);goal.w=edgeRight-goal.x;goal.h=edgeBottom-goal.y
250
325
  const keys=['x','y','w','h','alpha','arm']
251
- if(!tween || keys.some(key=>tween.to[key]!==goal[key])){
252
- const remaining=tween && tween.pressed===pressed?Math.max(0,tween.start+tween.duration-time):motionDuration
253
- tween={from:{...state},to:{...goal},start:time,duration:remaining,pressed}
326
+ let moving=false
327
+ if(spring && target && !pressed && !reduced?.matches){
328
+ // Exact critically damped solution: stable at variable frame rates, with
329
+ // velocity retained across target changes. No physics substep loop.
330
+ const dt=springTime===null?0:Math.max(0,(time-springTime)/1000)
331
+ springTime=time;springStart ??= time
332
+ const omega=48, decay=Math.exp(-omega*dt)
333
+ for(const key of keys){
334
+ const offset=state[key]-goal[key],speed=velocity[key] || 0
335
+ const coefficient=speed+omega*offset
336
+ state[key]=goal[key]+(offset+coefficient*dt)*decay
337
+ velocity[key]=(speed-omega*coefficient*dt)*decay
338
+ const tolerance=key==='alpha'?.002:.08
339
+ if(Math.abs(state[key]-goal[key])>tolerance || Math.abs(velocity[key])>tolerance*48)moving=true
340
+ }
341
+ // Stop together, with a bounded tail even after a busy main-thread frame.
342
+ if(!moving || time-springStart>=400){
343
+ Object.assign(state,goal);velocity={};spring=false;springTime=null;moving=false
344
+ }
345
+ }else{
346
+ spring=false;springTime=null;springStart=null
347
+ if(!tween || keys.some(key=>tween.to[key]!==goal[key])){
348
+ const remaining=tween && tween.pressed===pressed?Math.max(0,tween.start+tween.duration-time):motionDuration
349
+ tween={from:{...state},to:{...goal},start:time,duration:remaining,pressed}
350
+ }
351
+ // Keep instantaneous velocity available if a card morph interrupts entry.
352
+ const progress=reduced?.matches || settled || !tween.duration || (!state.alpha && !goal.alpha)?1:Math.min(1,(time-tween.start)/tween.duration)
353
+ const ease=1-(1-progress)**3
354
+ for(const key of keys){
355
+ state[key]=tween.from[key]+(goal[key]-tween.from[key])*ease
356
+ velocity[key]=progress<1?(goal[key]-tween.from[key])*3*(1-progress)**2*1000/tween.duration:0
357
+ }
358
+ moving=progress<1
254
359
  }
255
- // All channels share one endpoint; no separate thresholds or late position snap.
256
- const progress=reduced?.matches || settled || !tween.duration || (!state.alpha && !goal.alpha)?1:Math.min(1,(time-tween.start)/tween.duration)
257
- const ease=1-(1-progress)**3
258
- for(const key of keys)state[key]=tween.from[key]+(goal[key]-tween.from[key])*ease
259
- const moving=progress<1
260
360
  const x=state.x,y=state.y
261
361
  const right=state.x+state.w-7,bottom=state.y+state.h-7
262
362
  const positions=[[x,y],[right,y],[x,bottom],[right,bottom]]
@@ -298,7 +398,7 @@ function mountBodyTargetFrame(document, window, operation) {
298
398
  const focus=event=>{focused=event.target.matches?.(':focus-visible')?find(event.target):null;choose()}
299
399
  const blur=()=>{focused=null;choose()}
300
400
  const down=event=>{
301
- flushMove()
401
+ flushMove();clearGhost()
302
402
  pointer={x:event.clientX,y:event.clientY}
303
403
  if(event.pointerType==='touch')hovered=find(event.target)
304
404
  choose()
@@ -312,10 +412,11 @@ function mountBodyTargetFrame(document, window, operation) {
312
412
  }
313
413
  const up=event=>{release();if(event.pointerType==='touch')hovered=null;choose()}
314
414
  const reset=()=>{
315
- clearMove()
415
+ clearGhost();clearMove()
316
416
  target?.removeAttribute('data-prts-corner-target');hideFrame()
317
417
  hovered=focused=target=null;state=tween=bounds=exitGoal=retiringBounds=null
318
418
  imageRetiring=false
419
+ spring=false;springTime=springStart=null;velocity={};departed=null
319
420
  pressed=false;releasePending=false;settled=true;boundsDirty=hitDirty=false
320
421
  resizeObserver?.disconnect();window.cancelAnimationFrame(raf);raf=0
321
422
  frame.style.opacity='0';frame.removeAttribute('data-pressed')
@@ -355,11 +456,12 @@ function mountBodyTargetFrame(document, window, operation) {
355
456
  ]
356
457
  for(const [node,event,handler] of listeners)node.addEventListener(event,handler)
357
458
  return ()=>{
358
- clearMove()
459
+ clearGhost();clearMove()
359
460
  modalObserver?.disconnect();document.removeEventListener('toggle',modalToggle,true)
360
461
  disposed=true;target?.removeAttribute('data-prts-corner-target');hideFrame();window.cancelAnimationFrame(raf);resizeObserver?.disconnect()
361
462
  document.removeEventListener('scroll',scroll,true);window.removeEventListener('resize',reset)
362
463
  for(const [node,event,handler] of listeners)node.removeEventListener(event,handler)
464
+ ghost?.remove();ghost=null;ghostAnimations=[];ghostCorners=[]
363
465
  frame.remove()
364
466
  }
365
467
  }
@@ -2,12 +2,15 @@
2
2
  export function createCursorAdapter({ document, window, assets }) {
3
3
  const root = document?.documentElement
4
4
  let listening = false
5
+ let animate = true
5
6
  let pointerId
6
7
  let timer
8
+ let compact = false, pressedAt = 0
7
9
  let handPhase
8
10
  let gestureAssets = assets
9
11
  let textTarget
10
12
  let moveFrame, pendingMove
13
+ let textRange
11
14
  let lastPoint
12
15
  const heroTextSelector = '[data-phase="hero"] [class$="_titleGroup"]'
13
16
  const contentSelector = '[data-markdown], [data-slot*="markdown" i], [class*="markdown" i], :is([data-chat-flow-kind="user"], [data-chat-flow-kind="steering"], [data-submission-echo], [data-pending-steering]) [class$="_bubble"]'
@@ -23,14 +26,19 @@ export function createCursorAdapter({ document, window, assets }) {
23
26
  textTarget = nextTarget
24
27
  textTarget?.setAttribute('data-prts-text-hit', '')
25
28
  }
26
- function findTextHit(event) {
29
+ function prepareTextHit(target, semanticOnly = true) {
30
+ const content = target?.closest?.(`${contentSelector}, ${heroTextSelector}`)
31
+ if (!content || (!content.matches(heroTextSelector) && !content.closest('[data-chat-flow-kind], [data-message-role], [data-submission-echo], [data-pending-steering]')) || target.closest(controlSelector)) return {target}
32
+ const style = window.getComputedStyle(target)
33
+ if (style.userSelect === 'none' || (!semanticOnly && !style.cursor.includes('data:image/png'))) return {target}
34
+ return {target,content}
35
+ }
36
+ function findTextHit(event, semanticOnly = false, prepared) {
27
37
  if (event.pointerType !== 'mouse') return
28
38
  lastPoint = { clientX: event.clientX, clientY: event.clientY, pointerType: 'mouse' }
29
39
  const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
30
- const content = target?.closest?.(`${contentSelector}, ${heroTextSelector}`)
31
- if (!content || (!content.matches(heroTextSelector) && !content.closest('[data-chat-flow-kind], [data-message-role], [data-submission-echo], [data-pending-steering]')) || target.closest(controlSelector)) return
32
- const style = window.getComputedStyle(target)
33
- if (style.userSelect === 'none' || !style.cursor.includes('data:image/png')) return
40
+ const {content} = prepared?.target===target ? prepared : prepareTextHit(target,semanticOnly)
41
+ if (!content) return
34
42
  // Caret APIs return the nearest insertion point even over empty space.
35
43
  // Check its adjacent character rectangles before treating it as a text hit.
36
44
  const caret = document.caretPositionFromPoint?.(event.clientX, event.clientY)
@@ -38,8 +46,8 @@ export function createCursorAdapter({ document, window, assets }) {
38
46
  const node = caret?.offsetNode ?? fallback?.startContainer
39
47
  const offset = caret?.offset ?? fallback?.startOffset
40
48
  if (node?.nodeType !== 3 || !content.contains(node) || node.parentElement?.closest(controlSelector)) return
41
- if (window.getComputedStyle(node.parentElement).userSelect === 'none') return
42
- const range = document.createRange()
49
+ if (node.parentElement !== target && window.getComputedStyle(node.parentElement).userSelect === 'none') return
50
+ const range = textRange ||= document.createRange()
43
51
  for (const index of [offset - 1, offset]) {
44
52
  if (index < 0 || index >= node.length) continue
45
53
  range.setStart(node, index)
@@ -111,7 +119,7 @@ export function createCursorAdapter({ document, window, assets }) {
111
119
  lastHandFrame = undefined
112
120
  }
113
121
  // Cursor is inherited. Animating a large container still restyles its subtree.
114
- // Keep those background hits static, while controls retain native animation.
122
+ // Use one pressed pose for medium containers; keep larger backgrounds static.
115
123
  const maxAnimatedDescendants = 128
116
124
  function canAnimate(target) {
117
125
  return target && target.getElementsByTagName('*').length <= maxAnimatedDescendants
@@ -119,8 +127,9 @@ export function createCursorAdapter({ document, window, assets }) {
119
127
  function followHand(target) {
120
128
  if (stopIfHandChanged()) return
121
129
  if (target === handTarget && target !== textTarget) return
130
+ if(compact && handTarget && target!==handTarget){reset();return}
122
131
  restoreHandTarget()
123
- if (!canAnimate(target) || !handCursor || target === textTarget) return
132
+ if ((!compact && !canAnimate(target)) || !handCursor || target === textTarget) return
124
133
  const cursor = window.getComputedStyle(target).cursor
125
134
  if (!cursor.includes('data:image/png') || /(?:,\s*(?:text|grab|grabbing|not-allowed|no-drop)|resize)$/.test(cursor)) return
126
135
  handTarget = target
@@ -150,6 +159,7 @@ export function createCursorAdapter({ document, window, assets }) {
150
159
  handCursor = undefined
151
160
  root?.style.removeProperty('--prts-text-frame')
152
161
  handPhase = undefined
162
+ compact=false
153
163
  }
154
164
  function show(url) {
155
165
  const [x, y] = gestureAssets.hotspot
@@ -187,13 +197,19 @@ export function createCursorAdapter({ document, window, assets }) {
187
197
  updateTextHit(event)
188
198
  const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
189
199
  reset()
200
+ if (!animate) return
190
201
  const cursor = target ? window.getComputedStyle(target).cursor : ''
191
202
  if (!cursor.includes('data:image/png')) return
192
- if (/(?:,\s*(?:text|grab|grabbing|not-allowed|no-drop)|resize)$/.test(cursor) || !canAnimate(target)) return
203
+ if (/(?:,\s*(?:text|grab|grabbing|not-allowed|no-drop)|resize)$/.test(cursor)) return
204
+ const descendants=target.getElementsByTagName('*').length
205
+ if(descendants>256)return
206
+ compact=descendants>maxAnimatedDescendants
207
+ pressedAt=window.performance.now()
193
208
  gestureAssets = /,\s*pointer$/.test(cursor) && assets.link ? assets.link : assets
194
209
  pointerId = event.pointerId
195
- handCursor = cursor
210
+ handCursor = compact ? `url("${gestureAssets.hold}") ${gestureAssets.hotspot[0]} ${gestureAssets.hotspot[1]}, pointer` : cursor
196
211
  followHand(target)
212
+ if(compact){handPhase='hold';return}
197
213
  if (reduced()) { clearTimer(); show(gestureAssets.hold); return }
198
214
  handPhase = 'press'
199
215
  play(gestureAssets.press, () => {
@@ -212,6 +228,12 @@ export function createCursorAdapter({ document, window, assets }) {
212
228
  if (pointerId === undefined || event.pointerId !== pointerId || event.button !== 0) return
213
229
  pointerId = undefined
214
230
  if (reduced()) { reset(); return }
231
+ if(compact){
232
+ const remaining=80-(window.performance.now()-pressedAt)
233
+ if(remaining>0)timer=window.setTimeout(reset,remaining)
234
+ else reset()
235
+ return
236
+ }
215
237
  if (handPhase === 'press') return
216
238
  releaseHand()
217
239
  }
@@ -252,16 +274,26 @@ export function createCursorAdapter({ document, window, assets }) {
252
274
  if (event.target === window) resetInteraction()
253
275
  }
254
276
  function visibility() { if (document.hidden) resetInteraction() }
255
- const events = [['pointerdown', down], ['pointerup', up], ['pointermove', move], ['pointercancel', resetInteraction], ['blur', blur], ['dragstart', resetInteraction], ['pointerout', leave], ['scroll', refreshTextHit], ['resize', refreshTextHit]]
277
+ function over(event) {
278
+ if (animate || event.pointerType !== 'mouse') return
279
+ clearPendingMove()
280
+ applyMove(event)
281
+ }
282
+ const events = [['pointerdown', down], ['pointerup', up], ['pointermove', move], ['pointerover', over], ['pointercancel', resetInteraction], ['blur', blur], ['dragstart', resetInteraction], ['pointerout', leave], ['scroll', refreshTextHit], ['resize', refreshTextHit]]
256
283
  function dispose() {
257
284
  resetInteraction()
285
+ textRange=undefined
258
286
  if (!listening) return
259
287
  for (const [name, handler] of events) window.removeEventListener(name, handler, true)
260
288
  document.removeEventListener('visibilitychange', visibility)
261
289
  listening = false
262
290
  }
263
291
  return {
292
+ prepareTextHit,
293
+ textHit: (event, prepared) => findTextHit(event, true, prepared),
264
294
  update(preferences) {
295
+ if (animate && preferences.cursorAnimation === false) reset()
296
+ animate = preferences.cursorAnimation !== false
265
297
  if (!root || !assets || !preferences.enabled || preferences.cursor !== 'prts') { dispose(); return }
266
298
  if (listening) return
267
299
  for (const [name, handler] of events) window.addEventListener(name, handler, true)