@ash-qw/dsh-theme-prts 2.5.5 → 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 +26 -0
- package/README.en.md +41 -3
- package/README.md +40 -3
- package/THIRD_PARTY_NOTICES.md +6 -0
- package/THIRD_PARTY_NOTICES.zh-CN.md +6 -0
- package/lib/client.js +536 -316
- package/package.json +5 -3
- package/src/client/avatar-crop.js +16 -3
- package/src/client/background-region-editor.js +2 -1
- package/src/client/conversation-control-adapter.js +131 -20
- 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/index.js +5 -1
- package/src/client/native-preferences.js +8 -4
- package/src/client/page-controls.js +185 -0
- package/src/client/preferences.js +25 -5
- package/src/client/theme-controller-v2.js +6 -5
- package/src/client/theme-controller.js +3 -2
- package/src/client/theme-settings-workbench.js +133 -19
- package/src/client/theme-transfer.js +5 -4
- package/src/index.js +27 -5
- 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 +190 -8
- 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.
|
|
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",
|
|
@@ -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"
|
|
72
|
+
"dsh": "0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.5"
|
|
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',
|
|
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,16 +221,26 @@ 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
|
|
189
|
-
const scheduleOpen=schedule && [...schedule.children].find(child=>child.tagName==='BUTTON' && hasClassSuffix(child,'_cardOpen') && !child.matches(':disabled,[aria-disabled="true"]'))
|
|
228
|
+
const container=node.closest(cardSelector)
|
|
190
229
|
const button=node.closest('button')
|
|
191
|
-
|
|
192
|
-
|
|
230
|
+
let cardTarget=null
|
|
231
|
+
if(container){
|
|
232
|
+
const action=node.closest('button,a[href],[role="button"],[role="link"],summary,input:not([type="hidden"]),select,textarea')
|
|
233
|
+
const nested=action && action!==container && container.contains(action)
|
|
234
|
+
// Disabled controls must not fall back to their enclosing card.
|
|
235
|
+
if(node.closest(':disabled,[aria-disabled="true"],[inert]'))return null
|
|
236
|
+
const hitLayer=nested && action.tagName==='BUTTON' && (
|
|
237
|
+
container.matches('[data-presented-file]') && hasClassSuffix(action,'_cardPreview') ||
|
|
238
|
+
action.parentElement===container && container.matches('[data-tool="schedule_create"]') && hasClassSuffix(action,'_cardOpen'))
|
|
239
|
+
const openable=!container.matches('[data-tool="schedule_create"]') || [...container.children].some(child=>child.tagName==='BUTTON' && hasClassSuffix(child,'_cardOpen') && !child.matches(':disabled,[aria-disabled="true"]'))
|
|
240
|
+
cardTarget=nested && !hitLayer ? action : openable ? container : null
|
|
241
|
+
}
|
|
193
242
|
const pendingAction=operation.contains(button) && isPendingInteractionAction(button) ? button : null
|
|
194
|
-
const candidate=
|
|
243
|
+
const candidate=cardTarget || pendingAction || node.closest(selector)
|
|
195
244
|
return candidate && !candidate.matches(':disabled,[aria-disabled="true"]') && !candidate.closest('[inert]') ? candidate : null
|
|
196
245
|
}
|
|
197
246
|
const measure=(box)=>{bounds=box || target.getBoundingClientRect();targetArm=7+Math.max(0,Math.min(1,(Math.min(bounds.width,bounds.height)-36)/36))*5;boundsDirty=false}
|
|
@@ -200,20 +249,55 @@ function mountBodyTargetFrame(document, window, operation) {
|
|
|
200
249
|
const next=(hovered?.isConnected?hovered:null) || (focused?.isConnected?focused:null)
|
|
201
250
|
if(next!==target){
|
|
202
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={}}
|
|
203
280
|
const retire=!next && target && !target.isConnected && targetRetires && releasePending && bounds && !reduced?.matches
|
|
204
281
|
imageRetiring=false
|
|
205
282
|
retiringBounds=retire?{left:bounds.left,top:bounds.top,width:bounds.width,height:bounds.height}:null
|
|
206
283
|
target?.removeAttribute('data-prts-corner-target')
|
|
207
284
|
if(retire){hovered=focused=target=null;boundsDirty=false;resizeObserver?.disconnect();wake();return}
|
|
208
|
-
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
|
|
209
286
|
resizeObserver?.disconnect()
|
|
210
287
|
if(target)resizeObserver?.observe(target)
|
|
211
288
|
if(target){
|
|
212
289
|
targetRetires=hasClassSuffix(target,'_toBottom') || isPendingInteractionAction(target)
|
|
213
290
|
target.setAttribute('data-prts-corner-target','');showFrame()
|
|
214
291
|
measure(nextBounds);const box=bounds
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
}
|
|
217
301
|
frame.style.color=target.getAttribute('data-prts-conversation-control')==='danger'?window.getComputedStyle(target).getPropertyValue('--prts-red'):'var(--prts-corner-color, var(--prts-yellow))'
|
|
218
302
|
}
|
|
219
303
|
}
|
|
@@ -239,15 +323,40 @@ function mountBodyTargetFrame(document, window, operation) {
|
|
|
239
323
|
const edgeRight=pixel(goal.x+goal.w),edgeBottom=pixel(goal.y+goal.h)
|
|
240
324
|
goal.x=pixel(goal.x);goal.y=pixel(goal.y);goal.w=edgeRight-goal.x;goal.h=edgeBottom-goal.y
|
|
241
325
|
const keys=['x','y','w','h','alpha','arm']
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
245
359
|
}
|
|
246
|
-
// All channels share one endpoint; no separate thresholds or late position snap.
|
|
247
|
-
const progress=reduced?.matches || settled || !tween.duration || (!state.alpha && !goal.alpha)?1:Math.min(1,(time-tween.start)/tween.duration)
|
|
248
|
-
const ease=1-(1-progress)**3
|
|
249
|
-
for(const key of keys)state[key]=tween.from[key]+(goal[key]-tween.from[key])*ease
|
|
250
|
-
const moving=progress<1
|
|
251
360
|
const x=state.x,y=state.y
|
|
252
361
|
const right=state.x+state.w-7,bottom=state.y+state.h-7
|
|
253
362
|
const positions=[[x,y],[right,y],[x,bottom],[right,bottom]]
|
|
@@ -289,7 +398,7 @@ function mountBodyTargetFrame(document, window, operation) {
|
|
|
289
398
|
const focus=event=>{focused=event.target.matches?.(':focus-visible')?find(event.target):null;choose()}
|
|
290
399
|
const blur=()=>{focused=null;choose()}
|
|
291
400
|
const down=event=>{
|
|
292
|
-
flushMove()
|
|
401
|
+
flushMove();clearGhost()
|
|
293
402
|
pointer={x:event.clientX,y:event.clientY}
|
|
294
403
|
if(event.pointerType==='touch')hovered=find(event.target)
|
|
295
404
|
choose()
|
|
@@ -303,10 +412,11 @@ function mountBodyTargetFrame(document, window, operation) {
|
|
|
303
412
|
}
|
|
304
413
|
const up=event=>{release();if(event.pointerType==='touch')hovered=null;choose()}
|
|
305
414
|
const reset=()=>{
|
|
306
|
-
clearMove()
|
|
415
|
+
clearGhost();clearMove()
|
|
307
416
|
target?.removeAttribute('data-prts-corner-target');hideFrame()
|
|
308
417
|
hovered=focused=target=null;state=tween=bounds=exitGoal=retiringBounds=null
|
|
309
418
|
imageRetiring=false
|
|
419
|
+
spring=false;springTime=springStart=null;velocity={};departed=null
|
|
310
420
|
pressed=false;releasePending=false;settled=true;boundsDirty=hitDirty=false
|
|
311
421
|
resizeObserver?.disconnect();window.cancelAnimationFrame(raf);raf=0
|
|
312
422
|
frame.style.opacity='0';frame.removeAttribute('data-pressed')
|
|
@@ -346,11 +456,12 @@ function mountBodyTargetFrame(document, window, operation) {
|
|
|
346
456
|
]
|
|
347
457
|
for(const [node,event,handler] of listeners)node.addEventListener(event,handler)
|
|
348
458
|
return ()=>{
|
|
349
|
-
clearMove()
|
|
459
|
+
clearGhost();clearMove()
|
|
350
460
|
modalObserver?.disconnect();document.removeEventListener('toggle',modalToggle,true)
|
|
351
461
|
disposed=true;target?.removeAttribute('data-prts-corner-target');hideFrame();window.cancelAnimationFrame(raf);resizeObserver?.disconnect()
|
|
352
462
|
document.removeEventListener('scroll',scroll,true);window.removeEventListener('resize',reset)
|
|
353
463
|
for(const [node,event,handler] of listeners)node.removeEventListener(event,handler)
|
|
464
|
+
ghost?.remove();ghost=null;ghostAnimations=[];ghostCorners=[]
|
|
354
465
|
frame.remove()
|
|
355
466
|
}
|
|
356
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)
|