@ash-qw/dsh-theme-prts 2.6.0 → 2.6.1
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/CHANGELOG.md +14 -0
- package/README.en.md +38 -1
- package/README.md +37 -1
- package/THIRD_PARTY_NOTICES.md +6 -0
- package/THIRD_PARTY_NOTICES.zh-CN.md +6 -0
- package/lib/client.js +404 -295
- package/package.json +4 -2
- package/src/client/avatar-crop.js +16 -3
- package/src/client/background-region-editor.js +2 -1
- package/src/client/conversation-control-adapter.js +118 -16
- package/src/client/cursor-adapter.js +44 -12
- package/src/client/cursor-follower.js +271 -0
- package/src/client/cursor-semantics.js +74 -0
- package/src/client/page-controls.js +185 -0
- package/src/client/preferences.js +1 -1
- package/src/client/theme-controller-v2.js +3 -3
- package/src/client/theme-settings-workbench.js +51 -10
- package/src/client/theme-transfer.js +5 -4
- package/src/index.js +25 -4
- package/src/server/avatar-tools.js +1 -1
- package/src/server/background-preview.js +100 -0
- package/src/server/background-tools.js +109 -0
- package/src/server/backgrounds.js +15 -9
- package/src/server/theme-tool-status.js +13 -0
- package/src/server/tools.js +11 -6
- package/src/server/transfer-tools.js +166 -0
- package/src/styles/cursor.css +26 -2
- package/src/styles/refinement.css +1 -1
- package/src/styles/theme-settings-workbench.css +138 -6
- package/src/vendor/mouse-follower/LICENSE +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ash-qw/dsh-theme-prts",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.1",
|
|
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",
|
|
@@ -74,8 +74,10 @@
|
|
|
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',
|
|
107
|
-
if(busy||!hasCustom
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
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
|
|
31
|
-
if (!content
|
|
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
|
-
//
|
|
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)
|
|
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
|
-
|
|
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)
|