@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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import MouseFollower from 'mouse-follower'
|
|
2
|
+
import { gsap } from 'gsap'
|
|
3
|
+
import { createCursorSemantics } from './cursor-semantics.js'
|
|
4
|
+
import { createCursorAdapter as createNativeCursorAdapter } from './cursor-adapter.js'
|
|
5
|
+
|
|
6
|
+
// Mouse Follower owns the isolated DOM renderer. Use our Pointer Events lifecycle
|
|
7
|
+
// instead of its mouse/tween listeners: no easing, axis-dependent ticker or idle loop.
|
|
8
|
+
class DirectFollower extends MouseFollower {
|
|
9
|
+
bind() {}
|
|
10
|
+
}
|
|
11
|
+
MouseFollower.registerGSAP(gsap)
|
|
12
|
+
|
|
13
|
+
export function createCursorAdapter({document, window, assets}) {
|
|
14
|
+
const native = createNativeCursorAdapter({document, window, assets})
|
|
15
|
+
const root = document.documentElement
|
|
16
|
+
let follower, img, point, moveFrame, timer, family, resting, pressed = false, phase
|
|
17
|
+
let semantics, captureTarget, layerContext, stateObserver, watchedTarget, nativePicker
|
|
18
|
+
let textContext, renderedX, renderedY
|
|
19
|
+
let eventTarget, semanticTarget, semanticValue, pose, poseKind, dirty = true, hitDirty = true
|
|
20
|
+
let suspended = false, visible = false, currentUrl, ready = false
|
|
21
|
+
const staticPose=(rest,hotspot)=>({rest,hotspot})
|
|
22
|
+
const vectorPose=path=>staticPose('data:image/svg+xml,'+encodeURIComponent(
|
|
23
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="'+path+'" fill="none" stroke="#101820" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="'+path+'" fill="none" stroke="#eee8dc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'),[16,16])
|
|
24
|
+
|
|
25
|
+
const poses={
|
|
26
|
+
move:vectorPose('M16 3V29M3 16H29M12 7L16 3L20 7M12 25L16 29L20 25M7 12L3 16L7 20M25 12L29 16L25 20'),
|
|
27
|
+
crosshair:vectorPose('M16 4V12M16 20V28M4 16H12M20 16H28M16 15V17'),
|
|
28
|
+
text:staticPose(assets?.text,assets?.textHotspot),grab:staticPose(assets?.grab,[22,19]),
|
|
29
|
+
grabbing:staticPose(assets?.grabbing,[22,19]),unavailable:staticPose(assets?.unavailable,[7,7]),
|
|
30
|
+
...Object.fromEntries(Object.entries(assets?.resize || {}).map(([key,url])=>[key,staticPose(url,[16,16])])),
|
|
31
|
+
}
|
|
32
|
+
const resizeKinds={'n-resize':'ns','s-resize':'ns','ns-resize':'ns','row-resize':'ns',
|
|
33
|
+
'e-resize':'ew','w-resize':'ew','ew-resize':'ew','col-resize':'ew',
|
|
34
|
+
'ne-resize':'nesw','sw-resize':'nesw','nesw-resize':'nesw',
|
|
35
|
+
'nw-resize':'nwse','se-resize':'nwse','nwse-resize':'nwse'}
|
|
36
|
+
const reduced = () => root.dataset.prtsMotion === 'reduced' || window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
37
|
+
function stop() {
|
|
38
|
+
if (timer !== undefined) window.clearTimeout(timer)
|
|
39
|
+
timer = undefined
|
|
40
|
+
pressed = false
|
|
41
|
+
phase = undefined
|
|
42
|
+
}
|
|
43
|
+
function hide() {
|
|
44
|
+
stop()
|
|
45
|
+
if (follower) {
|
|
46
|
+
if(follower.el.matches(':popover-open'))follower.el.hidePopover()
|
|
47
|
+
follower.el.hidden = true
|
|
48
|
+
}
|
|
49
|
+
root.removeAttribute('data-prts-follower-visible')
|
|
50
|
+
semantics?.release()
|
|
51
|
+
layerContext=undefined
|
|
52
|
+
stateObserver?.disconnect();watchedTarget=undefined
|
|
53
|
+
visible = false
|
|
54
|
+
semanticTarget=pose=poseKind=textContext=renderedX=renderedY=undefined;dirty=true
|
|
55
|
+
}
|
|
56
|
+
function draw(url) {
|
|
57
|
+
if (!img || !url || currentUrl === url) return
|
|
58
|
+
currentUrl = url
|
|
59
|
+
img.src = url
|
|
60
|
+
}
|
|
61
|
+
function play(frames, done) {
|
|
62
|
+
if(timer !== undefined) window.clearTimeout(timer)
|
|
63
|
+
const started = window.performance.now()
|
|
64
|
+
const tick = () => {
|
|
65
|
+
timer = undefined
|
|
66
|
+
if (!visible) return
|
|
67
|
+
const elapsed = window.performance.now()-started
|
|
68
|
+
let index=0, deadline=frames[0].duration
|
|
69
|
+
while(index<frames.length-1 && elapsed>=deadline) deadline+=frames[++index].duration
|
|
70
|
+
if(index===frames.length-1){done();return}
|
|
71
|
+
draw(frames[index].url)
|
|
72
|
+
timer=window.setTimeout(tick, Math.max(1,deadline-(window.performance.now()-started)))
|
|
73
|
+
}
|
|
74
|
+
tick()
|
|
75
|
+
}
|
|
76
|
+
function release() {
|
|
77
|
+
phase='release'
|
|
78
|
+
play(family.release,()=>{stop();sync()})
|
|
79
|
+
}
|
|
80
|
+
function sync() {
|
|
81
|
+
if (!follower || !ready || !point || nativePicker || suspended || document.hidden || document.pointerLockElement) return hide()
|
|
82
|
+
const hit=hitDirty || !eventTarget?.isConnected ? document.elementFromPoint(point.x,point.y) : eventTarget
|
|
83
|
+
eventTarget=hit;hitDirty=false
|
|
84
|
+
if(!hit || hit.closest('iframe'))return hide()
|
|
85
|
+
const target=captureTarget?.isConnected?captureTarget:hit
|
|
86
|
+
if(target!==watchedTarget){
|
|
87
|
+
stateObserver.disconnect();watchedTarget=target
|
|
88
|
+
for(let el=target;el;el=el.parentElement)stateObserver.observe(el,{attributes:true,
|
|
89
|
+
attributeFilter:['style','class','disabled','aria-disabled','open','data-dragging','data-prts-emblem-sort-active','data-markdown','data-slot','data-phase','data-chat-flow-kind','data-message-role','data-submission-echo','data-pending-steering']})
|
|
90
|
+
}
|
|
91
|
+
if(dirty || target!==semanticTarget){
|
|
92
|
+
semanticValue=semantics.read(target)
|
|
93
|
+
textContext=undefined
|
|
94
|
+
semanticTarget=target;dirty=false
|
|
95
|
+
}
|
|
96
|
+
const raw=semanticValue
|
|
97
|
+
const kind=raw.split(',').at(-1).trim()
|
|
98
|
+
let nextFamily=assets
|
|
99
|
+
if(kind==='none'){
|
|
100
|
+
stop();follower.el.hidden=true
|
|
101
|
+
root.setAttribute('data-prts-follower-visible','');visible=true
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
if(kind==='pointer')nextFamily=assets.link
|
|
105
|
+
else if(['text','vertical-text'].includes(kind))nextFamily=poses.text
|
|
106
|
+
else if(resizeKinds[kind])nextFamily=poses[resizeKinds[kind]]
|
|
107
|
+
else if(['grab','grabbing','move','crosshair','cell'].includes(kind))nextFamily=poses[kind==='cell'?'crosshair':kind]
|
|
108
|
+
else if(kind==='not-allowed'||kind==='no-drop')nextFamily=poses.unavailable
|
|
109
|
+
else {
|
|
110
|
+
textContext ||= native.prepareTextHit(target)
|
|
111
|
+
if(native.textHit({target,clientX:point.x,clientY:point.y,pointerType:'mouse'},textContext))nextFamily=poses.text
|
|
112
|
+
}
|
|
113
|
+
if(family!==nextFamily && (!phase || !family?.press || !nextFamily.press)){stop();family=nextFamily}
|
|
114
|
+
resting=family.rest
|
|
115
|
+
const [x,y]=family.hotspot
|
|
116
|
+
if(pose!==family || poseKind!==kind){
|
|
117
|
+
img.style.left=-x+'px';img.style.top=-y+'px'
|
|
118
|
+
img.style.transform=kind==='vertical-text'?'rotate(90deg)':''
|
|
119
|
+
pose=family;poseKind=kind
|
|
120
|
+
}
|
|
121
|
+
if(!phase)draw(resting)
|
|
122
|
+
if(renderedX!==point.x || renderedY!==point.y){
|
|
123
|
+
follower.pos.x=point.x;follower.pos.y=point.y
|
|
124
|
+
follower.render(true)
|
|
125
|
+
renderedX=point.x;renderedY=point.y
|
|
126
|
+
}
|
|
127
|
+
follower.el.hidden=false
|
|
128
|
+
// A manual popover is non-interactive and stays above modal/top-layer UI.
|
|
129
|
+
const context=hit.closest('dialog,[popover]') || document.fullscreenElement
|
|
130
|
+
if(follower.el.showPopover && (!visible || context!==layerContext)){
|
|
131
|
+
if(context!==layerContext && follower.el.matches(':popover-open'))follower.el.hidePopover()
|
|
132
|
+
if(!follower.el.matches(':popover-open'))follower.el.showPopover()
|
|
133
|
+
}
|
|
134
|
+
layerContext=context
|
|
135
|
+
if(!visible)root.setAttribute('data-prts-follower-visible','')
|
|
136
|
+
visible=true
|
|
137
|
+
}
|
|
138
|
+
function flush() { moveFrame=undefined;sync() }
|
|
139
|
+
function move(event) {
|
|
140
|
+
if(event.pointerType!=='mouse')return
|
|
141
|
+
resumePicker(event)
|
|
142
|
+
if(suspended && !event.buttons)suspended=false
|
|
143
|
+
const unchanged=point?.x===event.clientX && point?.y===event.clientY && eventTarget===event.target
|
|
144
|
+
eventTarget=event.target;hitDirty=false
|
|
145
|
+
point={x:event.clientX,y:event.clientY}
|
|
146
|
+
if(pressed && !(event.buttons&1)){stop();draw(resting)}
|
|
147
|
+
if(!unchanged || dirty)if(moveFrame===undefined)moveFrame=window.requestAnimationFrame(flush)
|
|
148
|
+
}
|
|
149
|
+
function down(event) {
|
|
150
|
+
if(event.pointerType!=='mouse'){reset();return}
|
|
151
|
+
if(event.button!==0||event.isPrimary===false)return
|
|
152
|
+
const picker=event.target.closest?.('select,input[type="file"],input[type="color"]')
|
|
153
|
+
if(picker){nativePicker=picker;reset();return}
|
|
154
|
+
nativePicker=undefined
|
|
155
|
+
stop();dirty=true;eventTarget=event.target;hitDirty=false;point={x:event.clientX,y:event.clientY};sync()
|
|
156
|
+
if(!visible || follower.el.hidden)return
|
|
157
|
+
if(!family?.press)return
|
|
158
|
+
stop();pressed=true
|
|
159
|
+
if(reduced()){phase='hold';draw(family.hold);return}
|
|
160
|
+
phase='press'
|
|
161
|
+
play(family.press,()=>{
|
|
162
|
+
phase='hold';draw(family.hold)
|
|
163
|
+
if(!pressed)timer=window.setTimeout(release,48)
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
// Boundary updates only change the shared renderer's image; the browser
|
|
167
|
+
// cursor stays hidden throughout. Ordinary movement remains coalesced.
|
|
168
|
+
// Native pickers do not expose a reliable close event (including Escape).
|
|
169
|
+
// Resume only on pointer activity outside their owning control; refreshes
|
|
170
|
+
// and pointer activity on the select must never resurrect the overlay.
|
|
171
|
+
function resumePicker(event){
|
|
172
|
+
if(nativePicker && !nativePicker.contains(event.target))nativePicker=undefined
|
|
173
|
+
}
|
|
174
|
+
function keydown(event){
|
|
175
|
+
const picker=event.target.closest?.('select,input[type="file"],input[type="color"]')
|
|
176
|
+
if(picker && [' ','Enter','ArrowDown','ArrowUp','F4'].includes(event.key)){nativePicker=picker;reset()}
|
|
177
|
+
}
|
|
178
|
+
function over(event) {
|
|
179
|
+
if(event.pointerType!=='mouse')return
|
|
180
|
+
resumePicker(event)
|
|
181
|
+
if(moveFrame!==undefined)window.cancelAnimationFrame(moveFrame)
|
|
182
|
+
moveFrame=undefined
|
|
183
|
+
dirty=true;eventTarget=event.target;hitDirty=false
|
|
184
|
+
point={x:event.clientX,y:event.clientY}
|
|
185
|
+
sync()
|
|
186
|
+
}
|
|
187
|
+
function up(event) {
|
|
188
|
+
if(event.pointerType!=='mouse'||event.button!==0||!pressed)return
|
|
189
|
+
pressed=false;dirty=true
|
|
190
|
+
if(reduced()){stop();draw(resting);return}
|
|
191
|
+
if(phase!=='press')release()
|
|
192
|
+
}
|
|
193
|
+
function reset() {
|
|
194
|
+
if(moveFrame!==undefined)window.cancelAnimationFrame(moveFrame)
|
|
195
|
+
moveFrame=undefined;point=undefined;eventTarget=undefined;hitDirty=true;captureTarget=undefined;hide()
|
|
196
|
+
}
|
|
197
|
+
function blur(event){if(event.target===window)reset()}
|
|
198
|
+
function leave(event){if(!event.relatedTarget || event.relatedTarget.closest?.('iframe'))reset()}
|
|
199
|
+
function drag(){suspended=true;reset()}
|
|
200
|
+
function visibility(){if(document.hidden)reset()}
|
|
201
|
+
// Passive changes can also move elements beneath a stationary pointer.
|
|
202
|
+
// Re-hit then; ordinary pointer events already carry the browser's result.
|
|
203
|
+
function refresh(){
|
|
204
|
+
dirty=true;hitDirty=true
|
|
205
|
+
if(point && moveFrame===undefined)moveFrame=window.requestAnimationFrame(flush)
|
|
206
|
+
}
|
|
207
|
+
function capture(event){captureTarget=event.target;refresh()}
|
|
208
|
+
function uncapture(event){captureTarget=undefined;refresh(event)}
|
|
209
|
+
const events=[['pointermove',move],['pointerover',over],['pointerdown',down],['pointerup',up],
|
|
210
|
+
['pointercancel',reset],['pointerout',leave],['blur',blur],['dragstart',drag],
|
|
211
|
+
['scroll',refresh],['resize',refresh],['focusin',refresh],['click',refresh],
|
|
212
|
+
['gotpointercapture',capture],['lostpointercapture',uncapture],['contextmenu',drag],['keydown',keydown]]
|
|
213
|
+
function disposeFollower(){
|
|
214
|
+
reset()
|
|
215
|
+
for(const [name,fn]of events)window.removeEventListener(name,fn,true)
|
|
216
|
+
document.removeEventListener('visibilitychange',visibility)
|
|
217
|
+
document.removeEventListener('fullscreenchange',reset)
|
|
218
|
+
document.removeEventListener('pointerlockchange',reset)
|
|
219
|
+
if(follower){
|
|
220
|
+
window.clearTimeout(follower.visibleInt)
|
|
221
|
+
gsap.killTweensOf(follower.pos)
|
|
222
|
+
follower.destroy()
|
|
223
|
+
}
|
|
224
|
+
semantics?.dispose();semantics=undefined
|
|
225
|
+
stateObserver?.disconnect();stateObserver=watchedTarget=undefined
|
|
226
|
+
follower=img=family=currentUrl=undefined
|
|
227
|
+
ready=false
|
|
228
|
+
suspended=false
|
|
229
|
+
nativePicker=undefined
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
update(preferences){
|
|
233
|
+
const experimental=preferences.enabled && preferences.cursor==='prts-follower'
|
|
234
|
+
native.update({...preferences,enabled:preferences.enabled && !experimental})
|
|
235
|
+
if(!experimental){disposeFollower();return}
|
|
236
|
+
if(follower)return
|
|
237
|
+
follower=new DirectFollower({container:document.body,eventsTarget:document.documentElement,
|
|
238
|
+
className:'prts-cursor-follower',innerClassName:'prts-cursor-follower-inner',
|
|
239
|
+
speed:0,ease:'none',skewing:0,stateDetection:{},dataAttr:null,
|
|
240
|
+
visible:false,hideOnLeave:false,showTimeout:0,hideTimeout:0})
|
|
241
|
+
gsap.ticker.remove(follower.ticker)
|
|
242
|
+
stateObserver=new window.MutationObserver(refresh)
|
|
243
|
+
semantics=createCursorSemantics({document,window,onChange:refresh})
|
|
244
|
+
follower.el.setAttribute('popover','manual')
|
|
245
|
+
follower.el.hidden=true
|
|
246
|
+
follower.el.setAttribute('aria-hidden','true')
|
|
247
|
+
// Keep the native cursor until decoding finishes; setting src alone
|
|
248
|
+
// does not guarantee the first displayed frame is ready to paint.
|
|
249
|
+
const instance=follower, decoded=[]
|
|
250
|
+
for(const group of [assets,assets.link])for(const url of [group.rest,group.hold,...group.press.map(f=>f.url),...group.release.map(f=>f.url)]){
|
|
251
|
+
const image=new window.Image();image.src=url
|
|
252
|
+
decoded.push(image.decode())
|
|
253
|
+
}
|
|
254
|
+
for(const url of [...Object.values(assets.resize || {}),assets.text,assets.grab,assets.grabbing,assets.unavailable,poses.move.rest,poses.crosshair.rest]){
|
|
255
|
+
const image=new window.Image();image.src=url;decoded.push(image.decode())
|
|
256
|
+
}
|
|
257
|
+
img=document.createElement('img');img.alt='';img.draggable=false
|
|
258
|
+
follower.inner.append(img)
|
|
259
|
+
for(const [name,fn]of events)window.addEventListener(name,fn,true)
|
|
260
|
+
document.addEventListener('visibilitychange',visibility)
|
|
261
|
+
document.addEventListener('fullscreenchange',reset)
|
|
262
|
+
document.addEventListener('pointerlockchange',reset)
|
|
263
|
+
Promise.all(decoded).then(()=>{
|
|
264
|
+
if(follower!==instance)return
|
|
265
|
+
ready=true
|
|
266
|
+
sync()
|
|
267
|
+
}).catch(()=>{if(follower===instance)hide()})
|
|
268
|
+
},
|
|
269
|
+
dispose(){disposeFollower();native.dispose()},
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Preserve the host's cursor cascade as metadata while the experimental
|
|
2
|
+
// renderer suppresses the browser cursor. No hide/read/show cycle per move.
|
|
3
|
+
export function createCursorSemantics({document, window, onChange = () => {}}) {
|
|
4
|
+
const property='--prts-source-cursor'
|
|
5
|
+
const sheet=document.createElement('style')
|
|
6
|
+
sheet.dataset.prtsCursorSemantics=''
|
|
7
|
+
const inline=new Map()
|
|
8
|
+
let pending, disposed=false
|
|
9
|
+
function serialize(rules) {
|
|
10
|
+
let result=''
|
|
11
|
+
for(const rule of rules){
|
|
12
|
+
if(rule.selectorText?.includes('data-prts-follower-visible'))continue
|
|
13
|
+
const value=rule.style?.getPropertyValue('cursor')
|
|
14
|
+
const own=value?`${property}:${value}${rule.style.getPropertyPriority('cursor')?' !important':''};`:''
|
|
15
|
+
const children=rule.cssRules?serialize(rule.cssRules):''
|
|
16
|
+
if(!own&&!children)continue
|
|
17
|
+
if(rule.selectorText)result+=`${rule.selectorText}{${own}${children}}`
|
|
18
|
+
else if(rule.conditionText || /^@(media|supports|layer|container|scope)\b/.test(rule.cssText))result+=`${rule.cssText.slice(0,rule.cssText.indexOf('{'))}{${children}}`
|
|
19
|
+
}
|
|
20
|
+
return result
|
|
21
|
+
}
|
|
22
|
+
function rebuild(){
|
|
23
|
+
pending=undefined
|
|
24
|
+
if(disposed)return
|
|
25
|
+
let text=''
|
|
26
|
+
for(const source of document.styleSheets){
|
|
27
|
+
if(source.ownerNode===sheet || source.disabled)continue
|
|
28
|
+
try{
|
|
29
|
+
const rules=serialize(source.cssRules)
|
|
30
|
+
const media=source.media?.mediaText
|
|
31
|
+
text+=media?`@media ${media}{${rules}}`:rules
|
|
32
|
+
}catch{ /* Cross-origin sheets are not readable; semantic fallbacks remain. */ }
|
|
33
|
+
}
|
|
34
|
+
if(sheet.textContent!==text){sheet.textContent=text;onChange()}
|
|
35
|
+
}
|
|
36
|
+
function schedule(){if(!disposed && pending===undefined)pending=window.requestAnimationFrame(rebuild)}
|
|
37
|
+
function loaded(event){if(event.target?.matches?.('link[rel~="stylesheet"]'))schedule()}
|
|
38
|
+
const observer=new window.MutationObserver(records=>{
|
|
39
|
+
if(records.some(r=>r.target!==sheet && !sheet.contains(r.target)))schedule()
|
|
40
|
+
})
|
|
41
|
+
document.head.append(sheet)
|
|
42
|
+
rebuild()
|
|
43
|
+
observer.observe(document.head,{subtree:true,childList:true,characterData:true,attributes:true,attributeFilter:['disabled','media','href']})
|
|
44
|
+
document.addEventListener('load',loaded,true)
|
|
45
|
+
function restore(el,record){
|
|
46
|
+
if(el.style.getPropertyValue(property)===record.applied){
|
|
47
|
+
if(record.original)el.style.setProperty(property,record.original,record.priority)
|
|
48
|
+
else el.style.removeProperty(property)
|
|
49
|
+
}
|
|
50
|
+
if(record.masked && el.style.cursor==='none' && el.style.getPropertyPriority('cursor')==='important')el.style.setProperty('cursor',record.source,'important')
|
|
51
|
+
}
|
|
52
|
+
function read(target){
|
|
53
|
+
// Inline cursor updates (e.g. a body-wide resize gesture) outrank stylesheet
|
|
54
|
+
// rules. Mirror only the hit ancestry, without scanning the subtree.
|
|
55
|
+
const ancestry=new Set()
|
|
56
|
+
for(let el=target;el;el=el.parentElement){
|
|
57
|
+
ancestry.add(el)
|
|
58
|
+
let value=el.style.getPropertyValue('cursor'),priority=el.style.getPropertyPriority('cursor')
|
|
59
|
+
const old=inline.get(el)
|
|
60
|
+
if(old?.masked && value==='none' && priority==='important')value=old.source
|
|
61
|
+
if(!value){if(old){restore(el,old);inline.delete(el)}continue}
|
|
62
|
+
if(old && old.source===value && old.sourcePriority===priority)continue
|
|
63
|
+
if(old)restore(el,old)
|
|
64
|
+
const record={original:el.style.getPropertyValue(property),priority:el.style.getPropertyPriority(property),source:value,sourcePriority:priority,applied:value,masked:priority==='important'}
|
|
65
|
+
el.style.setProperty(property,value,priority)
|
|
66
|
+
if(record.masked)el.style.setProperty('cursor','none','important')
|
|
67
|
+
inline.set(el,record)
|
|
68
|
+
}
|
|
69
|
+
for(const [el,record]of inline)if(!ancestry.has(el)){restore(el,record);inline.delete(el)}
|
|
70
|
+
return window.getComputedStyle(target).getPropertyValue(property).trim()
|
|
71
|
+
}
|
|
72
|
+
function release(){for(const [el,record]of inline)restore(el,record);inline.clear()}
|
|
73
|
+
return {read,release,dispose(){disposed=true;observer.disconnect();document.removeEventListener('load',loaded,true);if(pending!==undefined)window.cancelAnimationFrame(pending);release();sheet.remove()}}
|
|
74
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Keep the existing form values/events as the model; render interactive UI in
|
|
2
|
+
// the page so native popup windows never take ownership of the cursor.
|
|
3
|
+
export function mountPageControls({panel,window}){
|
|
4
|
+
// Keep native controls usable in hosts without the popover API.
|
|
5
|
+
if(!panel || !window || typeof window.HTMLElement.prototype.showPopover!=='function')return {dispose(){}}
|
|
6
|
+
const document=panel.ownerDocument, cleanups=[]
|
|
7
|
+
let active, tooltip, tipTarget, originalDescription, restoringFocus=false, sequence=0
|
|
8
|
+
const hideTip=()=>{
|
|
9
|
+
if(!tooltip)return
|
|
10
|
+
tooltip.hidePopover();tooltip.remove();tooltip=undefined
|
|
11
|
+
if(originalDescription===null)tipTarget.removeAttribute('aria-describedby')
|
|
12
|
+
else tipTarget.setAttribute('aria-describedby',originalDescription)
|
|
13
|
+
tipTarget=undefined
|
|
14
|
+
}
|
|
15
|
+
const showTip=(target,text,anchor=target)=>{
|
|
16
|
+
hideTip()
|
|
17
|
+
if(!text)return
|
|
18
|
+
tooltip=document.createElement('div');tooltip.className='prts-page-tooltip'
|
|
19
|
+
tooltip.setAttribute('popover','manual');tooltip.setAttribute('role','tooltip')
|
|
20
|
+
tooltip.id='prts-control-tip-'+(++sequence);tooltip.textContent=text
|
|
21
|
+
tipTarget=target;originalDescription=target.getAttribute('aria-describedby')
|
|
22
|
+
target.setAttribute('aria-describedby',tooltip.id)
|
|
23
|
+
const container=target.closest('dialog') || panel;container.append(tooltip);tooltip.showPopover()
|
|
24
|
+
const rect=anchor.getBoundingClientRect(),size=tooltip.getBoundingClientRect()
|
|
25
|
+
const right=anchor!==target && rect.right+size.width+8<=window.innerWidth-8
|
|
26
|
+
tooltip.style.left=Math.max(8,Math.min(right?rect.right+8:rect.left,window.innerWidth-size.width-8))+'px'
|
|
27
|
+
const top=right?rect.top:rect.bottom+size.height+14<=window.innerHeight?rect.bottom+6:rect.top-size.height-6
|
|
28
|
+
tooltip.style.top=Math.max(8,Math.min(top,window.innerHeight-size.height-8))+'px'
|
|
29
|
+
}
|
|
30
|
+
const close=(focus=false)=>{
|
|
31
|
+
hideTip()
|
|
32
|
+
if(!active)return
|
|
33
|
+
const {popup,button}=active;active=undefined
|
|
34
|
+
popup.hidePopover();popup.remove();button.setAttribute('aria-expanded','false');button.removeAttribute('aria-controls')
|
|
35
|
+
if(focus){
|
|
36
|
+
restoringFocus=true
|
|
37
|
+
try{button.focus({preventScroll:true})}finally{restoringFocus=false}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const open=(button,role)=>{
|
|
41
|
+
close()
|
|
42
|
+
const popup=document.createElement('div')
|
|
43
|
+
popup.className='prts-page-popup';popup.setAttribute('popover','manual')
|
|
44
|
+
popup.setAttribute('role',role);popup.id='prts-page-popup-'+(++sequence)
|
|
45
|
+
popup.setAttribute('aria-label',button.getAttribute('aria-label'))
|
|
46
|
+
const container=button.closest('dialog') || panel
|
|
47
|
+
container.append(popup)
|
|
48
|
+
button.setAttribute('aria-controls',popup.id);button.setAttribute('aria-expanded','true')
|
|
49
|
+
active={popup,button}
|
|
50
|
+
popup.addEventListener('keydown',e=>{
|
|
51
|
+
e.stopPropagation()
|
|
52
|
+
if(e.key==='Escape'){e.preventDefault();close(true)}
|
|
53
|
+
if(e.key==='Tab' && role==='listbox')close(true)
|
|
54
|
+
})
|
|
55
|
+
return popup
|
|
56
|
+
}
|
|
57
|
+
const present=(popup,button)=>{
|
|
58
|
+
popup.showPopover()
|
|
59
|
+
const rect=button.getBoundingClientRect()
|
|
60
|
+
popup.style.minWidth=Math.min(260,Math.max(160,rect.width))+'px'
|
|
61
|
+
const size=popup.getBoundingClientRect()
|
|
62
|
+
popup.style.left=Math.max(8,Math.min(rect.left,window.innerWidth-size.width-8))+'px'
|
|
63
|
+
popup.style.top=Math.max(8,rect.bottom+size.height+6>window.innerHeight?rect.top-size.height-6:rect.bottom+6)+'px'
|
|
64
|
+
}
|
|
65
|
+
for(const control of panel.querySelectorAll('select,input[type="color"][data-prts-setting-color]')){
|
|
66
|
+
// A label forwards both hover and activation to its first button, making
|
|
67
|
+
// the entire settings row look clickable. These controls name themselves.
|
|
68
|
+
const label=control.parentElement
|
|
69
|
+
if(label.tagName==='LABEL'){
|
|
70
|
+
const row=document.createElement('div')
|
|
71
|
+
for(const {name,value} of label.attributes)row.setAttribute(name,value)
|
|
72
|
+
row.setAttribute('data-prts-page-control-row','')
|
|
73
|
+
row.append(...label.childNodes);label.replaceWith(row)
|
|
74
|
+
cleanups.push(()=>{label.append(...row.childNodes);row.replaceWith(label)})
|
|
75
|
+
}
|
|
76
|
+
const isSelect=control.tagName==='SELECT'
|
|
77
|
+
const button=document.createElement('button')
|
|
78
|
+
button.type='button';button.className='prts-page-control'
|
|
79
|
+
button.setAttribute('role',isSelect?'combobox':'button')
|
|
80
|
+
button.setAttribute('aria-label',control.getAttribute('aria-label') || control.parentElement.firstChild?.textContent?.trim() || '选择')
|
|
81
|
+
button.setAttribute('aria-haspopup',isSelect?'listbox':'dialog')
|
|
82
|
+
button.setAttribute('aria-expanded','false')
|
|
83
|
+
if(control.hasAttribute('aria-describedby'))button.setAttribute('aria-describedby',control.getAttribute('aria-describedby'))
|
|
84
|
+
const caption=document.createElement('span');caption.className='prts-page-caption';button.append(caption)
|
|
85
|
+
if(!isSelect && control.closest('[data-prts-session-flow-color-inputs]')){
|
|
86
|
+
const row=control.parentElement, name=row.querySelector('span'), value=row.querySelector('output')
|
|
87
|
+
const meta=document.createElement('span');meta.className='prts-page-color-meta'
|
|
88
|
+
if(name)meta.append(name)
|
|
89
|
+
if(value)meta.append(value)
|
|
90
|
+
button.append(meta)
|
|
91
|
+
cleanups.push(()=>{if(name)control.parentElement.prepend(name);if(value)control.parentElement.append(value)})
|
|
92
|
+
}
|
|
93
|
+
if(isSelect && Array.from(control.options).some(o=>o.dataset.description)){
|
|
94
|
+
const describe=()=>{if(!restoringFocus && active?.button!==button)showTip(button,control.selectedOptions[0]?.dataset.description)}
|
|
95
|
+
button.addEventListener('pointerenter',describe);button.addEventListener('focus',describe)
|
|
96
|
+
button.addEventListener('pointerleave',hideTip);button.addEventListener('blur',hideTip)
|
|
97
|
+
button.addEventListener('pointerdown',hideTip)
|
|
98
|
+
}
|
|
99
|
+
control.before(button)
|
|
100
|
+
control.setAttribute('data-prts-native-model','');control.tabIndex=-1
|
|
101
|
+
control.setAttribute('aria-hidden','true')
|
|
102
|
+
const sync=()=>{
|
|
103
|
+
button.disabled=control.disabled
|
|
104
|
+
const text=isSelect?(control.selectedOptions[0]?.textContent || ''):''
|
|
105
|
+
if(caption.textContent!==text){caption.textContent=text;if(tipTarget===button)showTip(button,control.selectedOptions[0]?.dataset.description)}
|
|
106
|
+
if(!isSelect){button.classList.add('prts-page-color');button.style.setProperty('--sample',control.value);button.title=control.value.toUpperCase()}
|
|
107
|
+
}
|
|
108
|
+
// Host updates use .value assignments, which do not emit change events.
|
|
109
|
+
const descriptor=Object.getOwnPropertyDescriptor(isSelect?window.HTMLSelectElement.prototype:window.HTMLInputElement.prototype,'value')
|
|
110
|
+
Object.defineProperty(control,'value',{configurable:true,get(){return descriptor.get.call(this)},set(value){descriptor.set.call(this,value);sync()}})
|
|
111
|
+
const observer=new window.MutationObserver(()=>{sync();if(control.disabled && active?.button===button)close()})
|
|
112
|
+
observer.observe(control,{attributes:true,childList:true,subtree:true})
|
|
113
|
+
control.addEventListener('change',sync)
|
|
114
|
+
const commit=value=>{
|
|
115
|
+
control.value=value
|
|
116
|
+
control.dispatchEvent(new window.Event('input',{bubbles:true}))
|
|
117
|
+
control.dispatchEvent(new window.Event('change',{bubbles:true}))
|
|
118
|
+
}
|
|
119
|
+
const show=()=>{
|
|
120
|
+
if(active?.button===button){close(true);return}
|
|
121
|
+
const popup=open(button,isSelect?'listbox':'dialog')
|
|
122
|
+
if(isSelect){
|
|
123
|
+
const options=Array.from(control.options)
|
|
124
|
+
const choose=option=>{close(true);commit(option.value)}
|
|
125
|
+
for(const option of options){
|
|
126
|
+
const row=document.createElement('button');row.type='button';row.role='option'
|
|
127
|
+
row.textContent=option.textContent;row.setAttribute('aria-label',option.textContent);row.disabled=option.disabled
|
|
128
|
+
row.setAttribute('aria-selected',String(option.selected))
|
|
129
|
+
if(option.dataset.description){
|
|
130
|
+
const describe=()=>showTip(row,option.dataset.description,popup)
|
|
131
|
+
row.addEventListener('pointerenter',describe);row.addEventListener('focus',describe)
|
|
132
|
+
row.addEventListener('pointerleave',hideTip);row.addEventListener('blur',hideTip)
|
|
133
|
+
}
|
|
134
|
+
row.addEventListener('click',()=>choose(option));popup.append(row)
|
|
135
|
+
}
|
|
136
|
+
popup.addEventListener('keydown',e=>{
|
|
137
|
+
const rows=Array.from(popup.querySelectorAll('button:not(:disabled)'))
|
|
138
|
+
let index=rows.indexOf(document.activeElement)
|
|
139
|
+
if(['ArrowDown','ArrowUp','Home','End'].includes(e.key)){
|
|
140
|
+
e.preventDefault()
|
|
141
|
+
index=e.key==='Home'?0:e.key==='End'?rows.length-1:(index+(e.key==='ArrowDown'?1:-1)+rows.length)%rows.length
|
|
142
|
+
rows[index]?.focus({preventScroll:true})
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
present(popup,button)
|
|
146
|
+
popup.querySelector('[aria-selected="true"]:not(:disabled)')?.focus({preventScroll:true})
|
|
147
|
+
}else{
|
|
148
|
+
popup.classList.add('prts-page-color-editor')
|
|
149
|
+
popup.innerHTML='<strong>自定义颜色</strong><i class="prts-page-sample"></i><label>HEX<input aria-label="HEX 颜色" maxlength="7" spellcheck="false"></label>'+['R','G','B'].map((name,i)=>'<label>'+name+'<input type="range" min="0" max="255" aria-label="'+name+' 通道" data-channel="'+i+'"><output></output></label>').join('')+'<footer><button type="button" data-cancel>取消</button><button type="button" data-apply>应用颜色</button></footer>'
|
|
150
|
+
let draft=control.value
|
|
151
|
+
const hex=popup.querySelector('input:not([type])'),ranges=Array.from(popup.querySelectorAll('[type=range]'))
|
|
152
|
+
const render=()=>{
|
|
153
|
+
hex.value=draft.toUpperCase();hex.removeAttribute('aria-invalid')
|
|
154
|
+
popup.querySelector('[data-apply]').disabled=false
|
|
155
|
+
popup.querySelector('i').style.background=draft
|
|
156
|
+
ranges.forEach((range,i)=>{range.value=parseInt(draft.slice(1+i*2,3+i*2),16);range.nextElementSibling.textContent=range.value})
|
|
157
|
+
}
|
|
158
|
+
hex.addEventListener('input',()=>{
|
|
159
|
+
const valid=/^#?[0-9a-f]{6}$/i.test(hex.value)
|
|
160
|
+
hex.setAttribute('aria-invalid',String(!valid));popup.querySelector('[data-apply]').disabled=!valid
|
|
161
|
+
if(valid){draft='#'+hex.value.replace('#','');render()}
|
|
162
|
+
})
|
|
163
|
+
ranges.forEach(range=>range.addEventListener('input',()=>{draft='#'+ranges.map(r=>Number(r.value).toString(16).padStart(2,'0')).join('');render()}))
|
|
164
|
+
popup.querySelector('[data-cancel]').onclick=()=>close(true)
|
|
165
|
+
popup.querySelector('[data-apply]').onclick=()=>{close(true);commit(draft)}
|
|
166
|
+
render();present(popup,button);hex.focus({preventScroll:true})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
button.addEventListener('click',e=>{e.preventDefault();show()})
|
|
170
|
+
button.addEventListener('keydown',e=>{
|
|
171
|
+
if(isSelect && ['ArrowDown','ArrowUp','Home','End'].includes(e.key)){e.preventDefault();e.stopPropagation();if(active?.button!==button)show()}
|
|
172
|
+
})
|
|
173
|
+
sync()
|
|
174
|
+
cleanups.push(()=>{observer.disconnect();control.removeEventListener('change',sync);delete control.value;button.remove();control.removeAttribute('data-prts-native-model');control.removeAttribute('aria-hidden');control.removeAttribute('tabindex')})
|
|
175
|
+
}
|
|
176
|
+
const outside=e=>{if(active&&!active.popup.contains(e.target)&&!active.button.contains(e.target))close()}
|
|
177
|
+
const scroll=e=>{if(active && (e.target===document || e.target.contains?.(active.button)) && !active.popup.contains(e.target))close()}
|
|
178
|
+
document.addEventListener('pointerdown',outside,true)
|
|
179
|
+
document.addEventListener('focusin',outside,true)
|
|
180
|
+
const dismiss=()=>close()
|
|
181
|
+
window.addEventListener('resize',dismiss)
|
|
182
|
+
document.addEventListener('scroll',scroll,true)
|
|
183
|
+
panel.addEventListener('close',dismiss,true)
|
|
184
|
+
return {dispose(){close();cleanups.forEach(fn=>fn());document.removeEventListener('pointerdown',outside,true);document.removeEventListener('focusin',outside,true);window.removeEventListener('resize',dismiss);document.removeEventListener('scroll',scroll,true);panel.removeEventListener('close',dismiss,true)}}
|
|
185
|
+
}
|
|
@@ -241,7 +241,7 @@ export function normalizePreferences(value) {
|
|
|
241
241
|
cornerColor: colorOr(input.cornerColor, DEFAULT_PREFERENCES.cornerColor),
|
|
242
242
|
cornerColorLight: colorOr(input.cornerColorLight, colorOr(input.cornerColor, DEFAULT_PREFERENCES.cornerColor)),
|
|
243
243
|
cornerCustomColorLight: colorOr(input.cornerCustomColorLight, colorOr(input.cornerCustomColor, DEFAULT_PREFERENCES.cornerCustomColor)),
|
|
244
|
-
cursor:
|
|
244
|
+
cursor: ['system', 'prts-follower'].includes(input.cursor) ? input.cursor : DEFAULT_PREFERENCES.cursor,
|
|
245
245
|
conversationParticleDensity: enumOr(legacyDensities.conversation, PARTICLE_DENSITIES, particlePreset.conversationParticleDensity),
|
|
246
246
|
heroParticleDensity: enumOr(legacyDensities.hero, PARTICLE_DENSITIES, particlePreset.heroParticleDensity),
|
|
247
247
|
particleTraversalSpeed: particleTraversalSpeedOr(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createCursorAdapter } from './cursor-
|
|
1
|
+
import { createCursorAdapter as createFollowerCursorAdapter } from './cursor-follower.js'
|
|
2
2
|
import { normalizePreferences } from './preferences.js'
|
|
3
3
|
|
|
4
4
|
export const SCHEME_COMMIT_PROGRESS = .15
|
|
@@ -80,7 +80,7 @@ export function createThemeController({ document, window, cssText, service, curs
|
|
|
80
80
|
return { apply() {}, sync() {}, refresh() {}, setTheme() {}, beginThemeTransition() { return gesture }, toggle() {}, dispose() {} }
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const cursor =
|
|
83
|
+
const cursor = createFollowerCursorAdapter({ document, window, assets: cursorAssets })
|
|
84
84
|
const root = document.documentElement
|
|
85
85
|
let style
|
|
86
86
|
let disposed = false
|
|
@@ -761,7 +761,7 @@ export function createThemeController({ document, window, cssText, service, curs
|
|
|
761
761
|
root.dataset.prtsTexture = preferences.texture
|
|
762
762
|
root.dataset.prtsGlass = preferences.glass
|
|
763
763
|
root.dataset.prtsMotion = preferences.motion
|
|
764
|
-
root.dataset.prtsCursor = preferences.cursor
|
|
764
|
+
root.dataset.prtsCursor = preferences.cursor === 'prts-follower' ? 'prts' : preferences.cursor
|
|
765
765
|
root.dataset.prtsParticlePattern = 'orthogonal'
|
|
766
766
|
root.dataset.prtsSessionFlow = preferences.sessionFlow ? 'on' : 'off'
|
|
767
767
|
root.dataset.prtsSessionGlow = preferences.sessionGlow ? 'on' : 'off'
|