@ash-qw/dsh-theme-prts 2.2.0 → 2.3.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 +22 -0
- package/README.en.md +10 -0
- package/README.md +10 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/THIRD_PARTY_NOTICES.zh-CN.md +1 -1
- package/lib/client.js +733 -62
- package/package.json +2 -2
- package/src/client/background-layout.js +59 -0
- package/src/client/background-region-editor.js +103 -0
- package/src/client/conversation-control-adapter.js +2 -0
- package/src/client/cursor-adapter.js +1 -1
- package/src/client/custom-background.js +23 -6
- package/src/client/index.js +10 -0
- package/src/client/native-preferences.js +31 -0
- package/src/client/operations-shell-v2.js +2 -2
- package/src/client/particle-field-adapter.js +67 -14
- package/src/client/preferences.js +3 -1
- package/src/client/session-card-reveal.js +125 -0
- package/src/client/sidebar-control-adapter.js +40 -35
- package/src/client/theme-controller-v2.js +2 -1
- package/src/client/theme-settings-workbench.js +13 -5
- package/src/client/theme-transfer-panel.js +98 -0
- package/src/client/theme-transfer.js +170 -0
- package/src/server/backgrounds.js +10 -6
- package/src/shared/background-region.js +21 -0
- package/src/styles/cursor.css +8 -0
- package/src/styles/custom-background.css +83 -13
- package/src/styles/prts.css +2 -2
- package/src/styles/refinement.css +28 -0
- package/src/styles/theme-settings-workbench.css +87 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ash-qw/dsh-theme-prts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@playwright/test": "1.55.1",
|
|
76
76
|
"esbuild": "^0.28.2",
|
|
77
77
|
"jsdom": "24.1.3",
|
|
78
|
-
"markdown-it": "^14.3.
|
|
78
|
+
"markdown-it": "^14.3.3"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@deepseek-ai/schemastery": "~3.18.4",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { backgroundPlacement } from '../shared/background-region.js'
|
|
2
|
+
|
|
3
|
+
export function createBackgroundLayout({window,document}) {
|
|
4
|
+
const root=document.documentElement
|
|
5
|
+
const raf=window.requestAnimationFrame?.bind(window) ?? (fn=>window.setTimeout(fn,16))
|
|
6
|
+
const caf=window.cancelAnimationFrame?.bind(window) ?? window.clearTimeout.bind(window)
|
|
7
|
+
let state={}, frame, disposed=false, enabled=false, observing=false, operation, sessions
|
|
8
|
+
const set=(name,value)=>{if(root.style.getPropertyValue(name)!==value) root.style.setProperty(name,value)}
|
|
9
|
+
const update=()=>{
|
|
10
|
+
frame=undefined
|
|
11
|
+
if(disposed||!observing)return
|
|
12
|
+
const nextOperation=document.querySelector('[data-prts-region="operation"]')
|
|
13
|
+
const nextSessions=document.querySelector('[data-prts-region="sessions"]')
|
|
14
|
+
if(operation!==nextOperation || sessions!==nextSessions){
|
|
15
|
+
resize?.disconnect(); operation=nextOperation;sessions=nextSessions
|
|
16
|
+
for(const node of [operation,sessions])if(node)resize?.observe(node)
|
|
17
|
+
}
|
|
18
|
+
const railWidth=parseFloat(window.getComputedStyle(root).getPropertyValue('--prts-nav-width'))||0
|
|
19
|
+
for(const slot of ['full','conversation','sidebar']){
|
|
20
|
+
const entry=state[slot]
|
|
21
|
+
const node=slot==='sidebar'?sessions:operation
|
|
22
|
+
const width=slot==='full'?window.innerWidth:(node?.clientWidth||0)+(slot==='sidebar'?railWidth:0)
|
|
23
|
+
const height=slot==='full'?window.innerHeight:node?.clientHeight||0
|
|
24
|
+
const active=entry?.region && width>0 && height>0
|
|
25
|
+
if(active){
|
|
26
|
+
const p=backgroundPlacement(entry.width,entry.height,width,height,entry.region)
|
|
27
|
+
set(`--prts-protected-${slot}-x`,`${p.x}px`)
|
|
28
|
+
set(`--prts-protected-${slot}-y`,`${p.y}px`)
|
|
29
|
+
if(slot==='sidebar')set('--prts-protected-sidebar-width',`${p.width}px`)
|
|
30
|
+
}else{
|
|
31
|
+
root.style.removeProperty(`--prts-protected-${slot}-x`)
|
|
32
|
+
root.style.removeProperty(`--prts-protected-${slot}-y`)
|
|
33
|
+
if(slot==='sidebar')root.style.removeProperty('--prts-protected-sidebar-width')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function schedule(){if(!disposed && observing && frame===undefined)frame=raf(update)}
|
|
38
|
+
const resize=window.ResizeObserver?new window.ResizeObserver(schedule):null
|
|
39
|
+
const mutations=new window.MutationObserver(()=>{
|
|
40
|
+
if(!operation?.isConnected || !sessions?.isConnected)schedule()
|
|
41
|
+
})
|
|
42
|
+
const attributes=new window.MutationObserver(schedule)
|
|
43
|
+
function clear(){
|
|
44
|
+
caf(frame);frame=undefined;resize?.disconnect();mutations.disconnect();attributes.disconnect();window.removeEventListener('resize',schedule)
|
|
45
|
+
operation=undefined;sessions=undefined;observing=false
|
|
46
|
+
for(const slot of ['full','conversation','sidebar'])for(const axis of ['x','y','width'])root.style.removeProperty(`--prts-protected-${slot}-${axis}`)
|
|
47
|
+
}
|
|
48
|
+
function reconcile(){
|
|
49
|
+
if(disposed || !enabled || !['full','conversation','sidebar'].some(slot=>state[slot]?.region)){clear();return}
|
|
50
|
+
if(!observing){
|
|
51
|
+
observing=true
|
|
52
|
+
mutations.observe(document.body||root,{childList:true,subtree:true})
|
|
53
|
+
attributes.observe(root,{attributes:true,attributeFilter:['data-prts-background-mode','data-prts-rail-mode']})
|
|
54
|
+
window.addEventListener('resize',schedule)
|
|
55
|
+
}
|
|
56
|
+
schedule()
|
|
57
|
+
}
|
|
58
|
+
return {update(value){state=value;reconcile()},setEnabled(value){enabled=value;reconcile()},dispose(){disposed=true;clear()}}
|
|
59
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { backgroundPlacement } from '../shared/background-region.js'
|
|
2
|
+
|
|
3
|
+
export function backgroundRegionMarkup(slot) {
|
|
4
|
+
return `<dialog data-prts-region-editor="${slot}" aria-labelledby="prts-region-title-${slot}">
|
|
5
|
+
<header><div><strong id="prts-region-title-${slot}">保护重要区域</strong><p>拖动框移动,拖动四边或四角缩放。横向优先保留中心,纵向优先保留顶端。</p></div><button type="button" data-prts-region-cancel aria-label="关闭保护区域设置">×</button></header>
|
|
6
|
+
<div data-prts-region-workspace><section data-prts-region-original><strong>原图与保护框</strong><div data-prts-region-stage><img alt="背景原图" draggable="false"><div data-prts-region-box tabindex="0" role="group" aria-label="保护区域,方向键移动"><span>保护区域</span>${['nw','ne','sw','se','n','e','s','w'].map((corner,i)=>`<button type="button" data-prts-region-handle="${corner}" aria-label="调整${['左上角','右上角','左下角','右下角','上边','右边','下边','左边'][i]},使用方向键"></button>`).join('')}</div><div data-prts-region-ruler="x"></div><div data-prts-region-ruler="y"></div></div><small>边缘刻度为图片坐标。可用方向键移动,Shift 加速。</small></section>
|
|
7
|
+
<section data-prts-region-samples><strong>窗口裁剪预览</strong><p>${slot==='sidebar' ? '按当前侧栏高度等比例预览,包含导航栏背景。' : '白框标出保护区域,调整时同步更新。'}</p><div data-prts-region-preview-grid>${(slot==='sidebar' ? [['narrow','较窄侧栏 · 264px'],['wide','较宽侧栏 · 420px']] : [['wide','横向窗口 · 16:9'],['narrow','纵向窗口 · 9:16']]).map(([kind,label])=>`<figure><figcaption>${label}</figcaption><div data-prts-region-preview="${kind}"><i></i></div><small data-prts-region-warning hidden>此比例无法完整显示保护区域:横向保中心,纵向保顶端。</small></figure>`).join('')}</div></section></div>
|
|
8
|
+
<footer><button type="button" data-prts-region-create>恢复默认框</button><button type="button" data-prts-region-clear>清除保护区域</button><span></span><button type="button" data-prts-region-cancel>取消</button><button type="button" data-prts-region-save>保存</button></footer><p data-prts-region-status role="status"></p>
|
|
9
|
+
</dialog>`
|
|
10
|
+
}
|
|
11
|
+
export function mountBackgroundRegionEditor({section,window,store}) {
|
|
12
|
+
let state=store.snapshot(), active, draft=null, revision, imageId, busy=false, disposed=false, drag
|
|
13
|
+
const clamp=(v,min,max)=>Math.max(min,Math.min(max,v))
|
|
14
|
+
const defaultRegion=()=>({x:.25,y:.2,width:.5,height:.6})
|
|
15
|
+
const editor=()=>active && section.querySelector(`[data-prts-region-editor="${active}"]`)
|
|
16
|
+
function draw(){
|
|
17
|
+
const el=editor(),entry=state[active];if(!el||!entry)return
|
|
18
|
+
const box=el.querySelector('[data-prts-region-box]')
|
|
19
|
+
box.hidden=!draft
|
|
20
|
+
if(draft)Object.assign(box.style,{left:`${draft.x*100}%`,top:`${draft.y*100}%`,width:`${draft.width*100}%`,height:`${draft.height*100}%`})
|
|
21
|
+
for(const axis of ['x','y']){
|
|
22
|
+
const ruler=el.querySelector(`[data-prts-region-ruler="${axis}"]`)
|
|
23
|
+
ruler.innerHTML=[0,25,50,75,100].filter(v=>!draft||Math.abs(v-draft[axis]*100)>8).map(v=>`<span style="${axis==='x'?'left':'top'}:${v}%">${v}</span>`).join('')
|
|
24
|
+
if(draft)ruler.innerHTML+=`<b style="${axis==='x'?'left':'top'}:${draft[axis]*100}%">${Math.round(draft[axis]*100)}%</b>`
|
|
25
|
+
}
|
|
26
|
+
const previews=[...el.querySelectorAll('[data-prts-region-preview]')]
|
|
27
|
+
const sidebarHeight=section.ownerDocument.querySelector('[data-prts-region="sessions"]')?.clientHeight||window.innerHeight
|
|
28
|
+
const railWidth=parseFloat(window.getComputedStyle(section.ownerDocument.documentElement).getPropertyValue('--prts-nav-width'))||0
|
|
29
|
+
// DSH 0.2.1-rc.2 clamps the sidebar track to 264–420px; use the same scale for both samples.
|
|
30
|
+
const sidebarScale=Math.min(240/Math.max(1,sidebarHeight),...previews.map(preview=>Math.max(1,preview.parentElement.clientWidth)/((preview.dataset.prtsRegionPreview==='narrow'?264:420)+railWidth)))
|
|
31
|
+
for(const preview of previews){
|
|
32
|
+
const narrow=preview.dataset.prtsRegionPreview==='narrow'
|
|
33
|
+
const w=active==='sidebar'?((narrow?264:420)+railWidth)*sidebarScale:Math.max(1,Math.min(narrow?112:200,preview.parentElement.clientWidth))
|
|
34
|
+
const h=active==='sidebar'?sidebarHeight*sidebarScale:w*(narrow?16/9:9/16)
|
|
35
|
+
const p=backgroundPlacement(entry.width,entry.height,w,h,draft)
|
|
36
|
+
Object.assign(preview.style,{width:`${w}px`,height:`${h}px`,backgroundImage:`url("/prts-theme-api/backgrounds/image/${entry.id}")`,backgroundSize:`${p.width}px ${p.height}px`,backgroundPosition:`${p.x}px ${p.y}px`})
|
|
37
|
+
const mark=preview.firstElementChild;mark.hidden=!draft
|
|
38
|
+
if(draft)Object.assign(mark.style,{left:`${p.x+draft.x*p.width}px`,top:`${p.y+draft.y*p.height}px`,width:`${draft.width*p.width}px`,height:`${draft.height*p.height}px`})
|
|
39
|
+
preview.parentElement.querySelector('[data-prts-region-warning]').hidden=!p.clipped
|
|
40
|
+
}
|
|
41
|
+
for(const button of el.querySelectorAll('button'))button.disabled=busy
|
|
42
|
+
}
|
|
43
|
+
const resize=window.ResizeObserver ? new window.ResizeObserver(()=>{if(!disposed)draw()}) : null
|
|
44
|
+
function close(){window.removeEventListener('resize',draw);resize?.disconnect();const el=editor();el?.querySelector('[data-prts-region-stage]').style.removeProperty('cursor');if(el?.open)el.close();if(active)section.querySelector(`[data-prts-background-region-open="${active}"]`)?.focus();active=undefined;drag=undefined}
|
|
45
|
+
function open(slot){
|
|
46
|
+
if(busy)return
|
|
47
|
+
close();const entry=state[slot];if(!entry?.width||!entry?.height)return
|
|
48
|
+
active=slot;revision=state.revision;imageId=entry.id;draft=entry.region?{...entry.region}:defaultRegion()
|
|
49
|
+
const el=editor();el.showModal();resize?.observe(el)
|
|
50
|
+
if(slot==='sidebar'){const sidebar=section.ownerDocument.querySelector('[data-prts-region="sessions"]');if(sidebar)resize?.observe(sidebar)}
|
|
51
|
+
window.addEventListener('resize',draw)
|
|
52
|
+
el.querySelector('[data-prts-region-stage]').style.width=`min(100%, ${Math.min(640,360*entry.width/entry.height)}px)`
|
|
53
|
+
el.querySelector('img').src=`/prts-theme-api/backgrounds/image/${entry.id}`
|
|
54
|
+
el.querySelector('[data-prts-region-status]').textContent=''
|
|
55
|
+
draw()
|
|
56
|
+
}
|
|
57
|
+
async function save(){
|
|
58
|
+
if(busy||!active)return
|
|
59
|
+
const el=editor(),slot=active,region=draft?{...draft}:null
|
|
60
|
+
busy=true;draw();el.querySelector('[data-prts-region-status]').textContent='正在保存…'
|
|
61
|
+
try{await store.saveRegion(slot,region,revision);if(!disposed){close();section.querySelector('[data-prts-background-status]').textContent='保护区域已保存,并同步到其他页面。';section.querySelector(`[data-prts-background-region-open="${slot}"]`).focus()}}
|
|
62
|
+
catch(error){if(!disposed)el.querySelector('[data-prts-region-status]').textContent=error.message+' 请取消后重新打开编辑。'}
|
|
63
|
+
finally{busy=false;if(!disposed)draw()}
|
|
64
|
+
}
|
|
65
|
+
const click=event=>{
|
|
66
|
+
const openButton=event.target.closest('[data-prts-background-region-open]');if(openButton){open(openButton.dataset.prtsBackgroundRegionOpen);return}
|
|
67
|
+
if(!active||busy)return
|
|
68
|
+
if(event.target.closest('[data-prts-region-cancel]'))close()
|
|
69
|
+
else if(event.target.closest('[data-prts-region-save]'))void save()
|
|
70
|
+
else if(event.target.closest('[data-prts-region-create]')){draft=defaultRegion();draw();editor().querySelector('[data-prts-region-box]').focus()}
|
|
71
|
+
else if(event.target.closest('[data-prts-region-clear]')){draft=null;draw()}
|
|
72
|
+
}
|
|
73
|
+
const down=event=>{
|
|
74
|
+
const el=editor(),stage=event.target.closest('[data-prts-region-stage]')
|
|
75
|
+
if(busy||!draft||!stage||!el?.contains(stage)||!event.target.closest('[data-prts-region-box]')||event.button!==0)return
|
|
76
|
+
event.preventDefault();const bounds=stage.getBoundingClientRect();if(!bounds.width||!bounds.height)return
|
|
77
|
+
const x=clamp((event.clientX-bounds.left)/bounds.width,0,1),y=clamp((event.clientY-bounds.top)/bounds.height,0,1)
|
|
78
|
+
drag={x,y,bounds,initial:{...draft},corner:event.target.dataset.prtsRegionHandle,id:event.pointerId}
|
|
79
|
+
stage.style.cursor=window.getComputedStyle(event.target).cursor
|
|
80
|
+
stage.setPointerCapture?.(event.pointerId);draw()
|
|
81
|
+
}
|
|
82
|
+
function adjust(initial,dx,dy,corner){
|
|
83
|
+
if(!corner)return {...initial,x:clamp(initial.x+dx,0,1-initial.width),y:clamp(initial.y+dy,0,1-initial.height)}
|
|
84
|
+
let left=initial.x,top=initial.y,right=left+initial.width,bottom=top+initial.height
|
|
85
|
+
if(corner.includes('w'))left=clamp(left+dx,0,right-.01);else if(corner.includes('e'))right=clamp(right+dx,left+.01,1)
|
|
86
|
+
if(corner.includes('n'))top=clamp(top+dy,0,bottom-.01);else if(corner.includes('s'))bottom=clamp(bottom+dy,top+.01,1)
|
|
87
|
+
return {x:left,y:top,width:/[ew]/.test(corner)?right-left:initial.width,height:/[ns]/.test(corner)?bottom-top:initial.height}
|
|
88
|
+
}
|
|
89
|
+
const move=event=>{if(!drag||event.pointerId!==drag.id)return;draft=adjust(drag.initial,(event.clientX-drag.bounds.left)/drag.bounds.width-drag.x,(event.clientY-drag.bounds.top)/drag.bounds.height-drag.y,drag.corner);draw()}
|
|
90
|
+
const up=event=>{if(drag&&event.pointerId===drag.id){if(event.type==='pointercancel')draft=drag.initial;drag=undefined;editor()?.querySelector('[data-prts-region-stage]').style.removeProperty('cursor');draw()}}
|
|
91
|
+
const key=event=>{
|
|
92
|
+
if(!active||busy)return
|
|
93
|
+
if(event.key==='Escape'){event.preventDefault();event.stopPropagation();close();return}
|
|
94
|
+
if(!draft||!event.target.closest('[data-prts-region-box]')||!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key))return
|
|
95
|
+
event.preventDefault();const step=event.shiftKey ? .05 : .01
|
|
96
|
+
draft=adjust(draft,event.key==='ArrowLeft'?-step:event.key==='ArrowRight'?step:0,event.key==='ArrowUp'?-step:event.key==='ArrowDown'?step:0,event.target.dataset.prtsRegionHandle);draw()
|
|
97
|
+
}
|
|
98
|
+
const cancel=event=>{if(event.target.matches('[data-prts-region-editor]')){event.preventDefault();event.stopPropagation();if(!busy)close()}}
|
|
99
|
+
section.addEventListener('cancel',cancel,true)
|
|
100
|
+
const handlers={click,pointerdown:down,pointermove:move,pointerup:up,pointercancel:up,keydown:key}
|
|
101
|
+
for(const [name,fn] of Object.entries(handlers))section.addEventListener(name,fn)
|
|
102
|
+
return {update(value,mode){state=value;if(active&&(state[active]?.id!==imageId||mode==='off'||(mode==='full')!==(active==='full')))close()},dispose(){disposed=true;close();section.removeEventListener('cancel',cancel,true);for(const [name,fn] of Object.entries(handlers))section.removeEventListener(name,fn)}}
|
|
103
|
+
}
|
|
@@ -14,6 +14,8 @@ function closestWithClassSuffix(node, suffix) {
|
|
|
14
14
|
function classify(button, operation) {
|
|
15
15
|
// The host's process disclosure owns its transparent, full-width hit area.
|
|
16
16
|
if (button.hasAttribute('data-turn-process')) return null
|
|
17
|
+
// Notification headers belong to the card surface, not standalone buttons.
|
|
18
|
+
if (button.parentElement?.matches('[data-turn-trigger]')) return null
|
|
17
19
|
// Changed-file cards own one outer surface; their header and rows stay native.
|
|
18
20
|
if (button.closest('[data-changed-files]')) return null
|
|
19
21
|
// File previews are transparent hit targets; the enclosing card owns its material and outline.
|
|
@@ -69,7 +69,7 @@ export function createCursorAdapter({ document, window, assets }) {
|
|
|
69
69
|
const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
|
|
70
70
|
if (target === resizeTarget && target.style.cursor === resizeApplied) return
|
|
71
71
|
clearResize()
|
|
72
|
-
if (!target) return
|
|
72
|
+
if (!target || target.closest('[data-prts-region-stage]')) return
|
|
73
73
|
const cursor = window.getComputedStyle(target).cursor
|
|
74
74
|
const direction = { 'n-resize': 'ns', 's-resize': 'ns', 'ns-resize': 'ns', 'row-resize': 'ns',
|
|
75
75
|
'e-resize': 'ew', 'w-resize': 'ew', 'ew-resize': 'ew', 'col-resize': 'ew',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createBackgroundLayout } from './background-layout.js'
|
|
2
|
+
import { mountBackgroundRegionEditor, backgroundRegionMarkup } from './background-region-editor.js'
|
|
1
3
|
import { opacityDialMarkup, mountOpacityDials } from './opacity-dial.js'
|
|
2
4
|
const BACKGROUND_ENDPOINT = '/prts-theme-api/backgrounds'
|
|
3
5
|
const backgroundLabels = { sidebar: '侧边栏', conversation: '会话正文', full: '完整背景' }
|
|
@@ -7,10 +9,12 @@ function backgroundImageUrl(entry) {
|
|
|
7
9
|
export function createBackgroundStore({ window, document }) {
|
|
8
10
|
let state = { revision: -1, sidebar: null, conversation: null, full: null }, disposed = false, events, refreshing
|
|
9
11
|
const listeners = new Set()
|
|
12
|
+
const layout = createBackgroundLayout({window,document})
|
|
10
13
|
const abort = window.AbortController ? new window.AbortController() : null
|
|
11
14
|
const apply = value => {
|
|
12
15
|
if (disposed || value.revision < state.revision) return
|
|
13
16
|
state = value
|
|
17
|
+
layout.update(state)
|
|
14
18
|
for (const slot of Object.keys(backgroundLabels)) {
|
|
15
19
|
const url = backgroundImageUrl(state[slot])
|
|
16
20
|
const root = document.documentElement
|
|
@@ -48,12 +52,18 @@ export function createBackgroundStore({ window, document }) {
|
|
|
48
52
|
window.addEventListener('focus', focus)
|
|
49
53
|
return {
|
|
50
54
|
snapshot: () => state, refresh,
|
|
55
|
+
setEnabled: value => layout.setEnabled(value),
|
|
51
56
|
subscribe(fn) { listeners.add(fn); fn(state); return () => listeners.delete(fn) },
|
|
52
57
|
async save(slot, image, revision = state.revision) {
|
|
53
58
|
try { return await request({ slot, image, revision }) }
|
|
54
59
|
catch (error) { await refresh().catch(() => {}); throw error }
|
|
55
60
|
},
|
|
61
|
+
async saveRegion(slot, region, revision = state.revision) {
|
|
62
|
+
try { return await request({ slot, region, revision }) }
|
|
63
|
+
catch (error) { await refresh().catch(() => {}); throw error }
|
|
64
|
+
},
|
|
56
65
|
dispose() {
|
|
66
|
+
layout.dispose()
|
|
57
67
|
document.documentElement.style.removeProperty('--prts-sidebar-image-ratio')
|
|
58
68
|
disposed = true; abort?.abort(); events?.close(); listeners.clear(); window.removeEventListener('focus', focus)
|
|
59
69
|
for (const slot of Object.keys(backgroundLabels)) {
|
|
@@ -63,7 +73,7 @@ export function createBackgroundStore({ window, document }) {
|
|
|
63
73
|
},
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
|
-
export function backgroundMarkup() {
|
|
76
|
+
export function backgroundMarkup(heroContrastControl = '') {
|
|
67
77
|
return `<section data-prts-background-settings>
|
|
68
78
|
<header><strong>会话背景</strong><small>分区使用不同图片,或让一张完整背景贯穿侧边栏与正文。</small></header>
|
|
69
79
|
<div data-prts-background-modes role="group" aria-label="背景模式">
|
|
@@ -71,12 +81,16 @@ export function backgroundMarkup() {
|
|
|
71
81
|
</div>
|
|
72
82
|
<div data-prts-background-images hidden>${Object.entries(backgroundLabels).map(([slot,label]) => `<article data-prts-background-card="${slot}" hidden>
|
|
73
83
|
<div data-prts-background-card-heading><strong>${label}</strong><button type="button" data-prts-background-remove="${slot}" aria-label="移除${label}图片" hidden>移除</button></div>
|
|
74
|
-
<
|
|
84
|
+
<div data-prts-background-image-controls="${slot}"><span data-prts-background-thumbnail="${slot}" aria-hidden="true"></span><span data-prts-background-empty>尚未选择图片</span>
|
|
85
|
+
<div data-prts-background-image-actions><button type="button" data-prts-background-region-open="${slot}" hidden><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M7 3H3v4m10-4h4v4M3 13v4h4m10-4v4h-4"/><rect x="6.5" y="6.5" width="7" height="7" rx="1.5"/></svg><span>保护区域</span></button><button type="button" data-prts-background-upload="${slot}" aria-label="上传或替换${label}"><svg viewBox="0 0 20 20" aria-hidden="true"><rect x="3" y="3" width="14" height="14" rx="3"/><path d="m4 14 4-4 3 3 2-2 4 4"/><circle cx="12.5" cy="7" r="1"/></svg><span data-prts-background-caption="${slot}">选择图片</span></button></div>
|
|
86
|
+
</div>
|
|
87
|
+
${backgroundRegionMarkup(slot)}
|
|
75
88
|
<input type="file" data-prts-background-file="${slot}" accept="image/png,image/jpeg,image/webp,image/avif" hidden>
|
|
76
89
|
</article>`).join('')}</div>
|
|
77
90
|
<label data-prts-background-shade hidden><span>背景淡化 <output></output></span><input type="range" min="0" max="90" step="5" aria-label="背景淡化程度"><small>调高后背景更淡,接近当前主题底色。</small></label>
|
|
91
|
+
<div data-prts-background-hero-setting hidden>${heroContrastControl}</div>
|
|
78
92
|
${opacityDialMarkup()}
|
|
79
|
-
<small data-prts-background-help>支持 PNG / JPEG / WebP / AVIF · 最大 10 MB<br
|
|
93
|
+
<small data-prts-background-help>支持 PNG / JPEG / WebP / AVIF · 最大 10 MB<br>图片铺满,可框选重要区域优先保留。切换模式保留素材,并在当前 DSH 实例同步。</small><p data-prts-background-status role="status"></p>
|
|
80
94
|
</section>`
|
|
81
95
|
}
|
|
82
96
|
export function mountBackgroundSettings({ panel, window, store, onChange }) {
|
|
@@ -85,9 +99,11 @@ export function mountBackgroundSettings({ panel, window, store, onChange }) {
|
|
|
85
99
|
let disposed = false, busy = false, preferences = {}, state = store.snapshot()
|
|
86
100
|
const status = section.querySelector('[data-prts-background-status]')
|
|
87
101
|
const range = section.querySelector('input[type="range"]')
|
|
102
|
+
const regions = mountBackgroundRegionEditor({section,window,store})
|
|
88
103
|
const dials = mountOpacityDials({ section, window, onChange })
|
|
89
104
|
function render() {
|
|
90
105
|
dials.update(preferences, state)
|
|
106
|
+
regions.update(state, preferences.backgroundMode || 'off')
|
|
91
107
|
const mode = preferences.backgroundMode || 'off'
|
|
92
108
|
section.querySelector('[data-prts-background-images]').hidden = mode === 'off'
|
|
93
109
|
section.querySelector('[data-prts-background-shade]').hidden = mode === 'off'
|
|
@@ -96,10 +112,11 @@ export function mountBackgroundSettings({ panel, window, store, onChange }) {
|
|
|
96
112
|
for (const slot of Object.keys(backgroundLabels)) {
|
|
97
113
|
const url = backgroundImageUrl(state[slot])
|
|
98
114
|
section.querySelector(`[data-prts-background-card="${slot}"]`).hidden = mode === 'off' || (mode === 'full') !== (slot === 'full')
|
|
99
|
-
section.querySelector(`[data-prts-background-
|
|
115
|
+
section.querySelector(`[data-prts-background-image-controls="${slot}"]`).toggleAttribute('data-has-image', Boolean(url))
|
|
100
116
|
section.querySelector(`[data-prts-background-thumbnail="${slot}"]`).style.backgroundImage = url ? `url("${url}")` : ''
|
|
101
|
-
section.querySelector(`[data-prts-background-caption="${slot}"]`).textContent = url ? '替换图片' : '
|
|
117
|
+
section.querySelector(`[data-prts-background-caption="${slot}"]`).textContent = url ? '替换图片' : '选择图片'
|
|
102
118
|
section.querySelector(`[data-prts-background-remove="${slot}"]`).hidden = !url
|
|
119
|
+
section.querySelector(`[data-prts-background-region-open="${slot}"]`).hidden = !url
|
|
103
120
|
}
|
|
104
121
|
for (const button of section.querySelectorAll('[data-prts-background-upload], [data-prts-background-remove]')) button.disabled = busy
|
|
105
122
|
}
|
|
@@ -138,6 +155,6 @@ export function mountBackgroundSettings({ panel, window, store, onChange }) {
|
|
|
138
155
|
const unsubscribe = store.subscribe(value => { state = value; render() })
|
|
139
156
|
return {
|
|
140
157
|
update(value) { preferences = value; render() },
|
|
141
|
-
dispose() { disposed = true; dials.dispose(); unsubscribe(); section.removeEventListener('click', click); section.removeEventListener('change', change); section.removeEventListener('input', input) },
|
|
158
|
+
dispose() { disposed = true; regions.dispose(); dials.dispose(); unsubscribe(); section.removeEventListener('click', click); section.removeEventListener('change', change); section.removeEventListener('input', input) },
|
|
142
159
|
}
|
|
143
160
|
}
|
package/src/client/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createThemeTransfer } from './theme-transfer.js'
|
|
1
2
|
import { createBackgroundStore } from './custom-background.js'
|
|
2
3
|
import { createNativePreferences } from './native-preferences.js'
|
|
3
4
|
import { createComposerGlassSurface } from './composer-glass-surface.js'
|
|
@@ -91,6 +92,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
91
92
|
cssText,
|
|
92
93
|
service: themeService,
|
|
93
94
|
cursorAssets: assets.cursor,
|
|
95
|
+
onSchemeChange() { particleField.repaintScheme() },
|
|
94
96
|
onTransitionStateChange(active) {
|
|
95
97
|
particleField.setSchemeTransitionActive(active)
|
|
96
98
|
},
|
|
@@ -111,6 +113,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
111
113
|
const adapter = createRc7Adapter({ document })
|
|
112
114
|
const hostGeometry = createHostGeometryAdapter({ document, window })
|
|
113
115
|
const connection = contextService(ctx, 'connection')
|
|
116
|
+
let themeTransfer
|
|
114
117
|
let operations
|
|
115
118
|
let mountObserver
|
|
116
119
|
let mountFrame
|
|
@@ -176,6 +179,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
const disposeVisualState = () => {
|
|
182
|
+
backgroundStore.setEnabled(false)
|
|
179
183
|
particleField.update({ ...preferences, enabled: false })
|
|
180
184
|
startup.stop({ restoreFocus: false })
|
|
181
185
|
stopStartupReadyWatch()
|
|
@@ -198,9 +202,13 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
198
202
|
disposeVisualState()
|
|
199
203
|
return
|
|
200
204
|
}
|
|
205
|
+
backgroundStore.setEnabled(preferences.backgroundMode !== 'off')
|
|
201
206
|
if (!themeAlreadyApplied) theme.apply(preferences)
|
|
202
207
|
operations ||= createOperationsShell({
|
|
203
208
|
document, window, assets, adapter, hostGeometry, customEmblemStore, avatarStore, backgroundStore,
|
|
209
|
+
themeTransfer: themeTransfer ||= createThemeTransfer({ window, getPreferences: () => nativePreferences.value,
|
|
210
|
+
savePreferences: (value, revision) => nativePreferences.replace(value, revision),
|
|
211
|
+
checkWritable: () => nativePreferences.checkWritable(), checkExport: () => nativePreferences.checkReadable(), themeVersion: version }),
|
|
204
212
|
onSchemeToggle(next, interaction) {
|
|
205
213
|
return theme.setTheme(next, { ...interaction, animate: true })
|
|
206
214
|
},
|
|
@@ -347,6 +355,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
347
355
|
} else {
|
|
348
356
|
applyVisualState()
|
|
349
357
|
}
|
|
358
|
+
const stopBackgroundRedraw = backgroundStore.subscribe(() => { if (!safeMode) particleField.update(preferences) })
|
|
350
359
|
const bindNativePreferences = settingsCtx => {
|
|
351
360
|
const forms = contextService(settingsCtx, 'configForms')
|
|
352
361
|
if (forms) settingsCtx.effect(() => nativePreferences.bind(forms.get('theme-prts')))
|
|
@@ -359,6 +368,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
359
368
|
statusSource.dispose()
|
|
360
369
|
customEmblemStore.dispose()
|
|
361
370
|
avatarStore.dispose()
|
|
371
|
+
stopBackgroundRedraw()
|
|
362
372
|
backgroundStore.dispose()
|
|
363
373
|
particleField.dispose()
|
|
364
374
|
startup.dispose({ restoreFocus: false })
|
|
@@ -69,6 +69,37 @@ export function createNativePreferences({ storage, onChange }) {
|
|
|
69
69
|
}
|
|
70
70
|
return {
|
|
71
71
|
get value() { return current },
|
|
72
|
+
checkReadable() {
|
|
73
|
+
const s = snapshot()
|
|
74
|
+
if (disposed || s?.status !== 'ready') throw Error('请等待 DSH 设置加载完成')
|
|
75
|
+
if (busy || Object.keys(pending).length || Object.keys(failed).length) throw Error('请先等待当前设置保存成功')
|
|
76
|
+
return s.revision
|
|
77
|
+
},
|
|
78
|
+
checkWritable() {
|
|
79
|
+
const revision = this.checkReadable()
|
|
80
|
+
if (!writable(snapshot())) throw Error('当前连接无法保存主题设置')
|
|
81
|
+
return revision
|
|
82
|
+
},
|
|
83
|
+
async replace(next, expectedRevision) {
|
|
84
|
+
this.checkWritable()
|
|
85
|
+
const s = snapshot()
|
|
86
|
+
if (s.revision !== expectedRevision) throw Error('设置已在其他页面更新,请重新导入')
|
|
87
|
+
const normalized = normalizePreferences(next)
|
|
88
|
+
busy = true
|
|
89
|
+
phase = 'saving'; emit()
|
|
90
|
+
try {
|
|
91
|
+
const ops = Object.entries(normalized).map(([key, value]) => ({ op: 'set', path: [key], value }))
|
|
92
|
+
ops.push({ op: 'set', path: ['preferencesInitialized'], value: true })
|
|
93
|
+
if (await form.mutate(ops, s.revision) !== true) throw Error('DSH 未接受导入设置,请重试')
|
|
94
|
+
phase = 'saved'
|
|
95
|
+
} catch (error) {
|
|
96
|
+
phase = 'error'
|
|
97
|
+
throw error
|
|
98
|
+
} finally {
|
|
99
|
+
busy = false
|
|
100
|
+
receive()
|
|
101
|
+
}
|
|
102
|
+
},
|
|
72
103
|
bind(next) {
|
|
73
104
|
unsubscribe?.(); form = next
|
|
74
105
|
unsubscribe = form.subscribe(receive)
|
|
@@ -46,7 +46,7 @@ export function createOperationsShell({
|
|
|
46
46
|
onSchemeToggle = () => {},
|
|
47
47
|
onSchemeDragStart = () => undefined,
|
|
48
48
|
onThemeDisable = () => {},
|
|
49
|
-
customEmblemStore, avatarStore, backgroundStore,
|
|
49
|
+
customEmblemStore, avatarStore, backgroundStore, themeTransfer,
|
|
50
50
|
onPreferenceChange = () => {},
|
|
51
51
|
onPreferencePreview = () => {},
|
|
52
52
|
onVisualReset = () => {},
|
|
@@ -481,7 +481,7 @@ export function createOperationsShell({
|
|
|
481
481
|
trigger: shell.querySelector('[data-prts-rail-brand]'),
|
|
482
482
|
panel: shell.querySelector('[data-prts-theme-settings]'),
|
|
483
483
|
backdrop: shell.querySelector('[data-prts-settings-backdrop]'),
|
|
484
|
-
customEmblemStore, avatarStore, backgroundStore,
|
|
484
|
+
customEmblemStore, avatarStore, backgroundStore, themeTransfer,
|
|
485
485
|
onPreferenceChange,
|
|
486
486
|
onPreferencePreview,
|
|
487
487
|
onResetVisual: onVisualReset,
|
|
@@ -432,7 +432,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
432
432
|
const densityProfile = density => PARTICLE_DENSITY_PROFILES[density] ?? PARTICLE_DENSITY_PROFILES.standard
|
|
433
433
|
const particleRadius = density => densityProfile(density ?? activeDensity()).radius
|
|
434
434
|
const fieldEnabled = () => heroActive || (preferences.conversationStyle !== 'deck-chat' && preferences.conversationEmblem !== false)
|
|
435
|
-
const canRenderFrame = () => fieldEnabled() && !disposed && documentVisible && canvasVisible && !hydrationPaused && !startupPaused && !schemeTransitionPaused && !resizing
|
|
435
|
+
const canRenderFrame = (paintOnly = false) => fieldEnabled() && !disposed && documentVisible && canvasVisible && !hydrationPaused && !startupPaused && (paintOnly || !schemeTransitionPaused) && !resizing
|
|
436
436
|
const layoutWidth = () => width
|
|
437
437
|
const layoutHeight = () => height
|
|
438
438
|
const layoutShiftX = () => 0
|
|
@@ -1134,12 +1134,25 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1134
1134
|
resizeTimer = window.setTimeout?.(finishResizeTransaction, RESIZE_SETTLE_MS)
|
|
1135
1135
|
}
|
|
1136
1136
|
|
|
1137
|
+
function resizeHeroTransition(nextSize) {
|
|
1138
|
+
// Keep live positions and velocities; the spring follows the new destination.
|
|
1139
|
+
// Snapping to particle.target here would restore the old hero mid-explosion.
|
|
1140
|
+
clearResizeState()
|
|
1141
|
+
pointerActive = false
|
|
1142
|
+
resizeCommits += applyFieldSize(nextSize) ? 1 : 0
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1137
1145
|
function onOperationResize(entries = []) {
|
|
1138
1146
|
if (!operation || !canvas) return
|
|
1139
1147
|
resizeObservations += 1
|
|
1140
1148
|
const entry = entries.find(candidate => candidate.target === operation) ?? entries[0]
|
|
1141
1149
|
const contentRect = entry?.contentRect
|
|
1142
1150
|
pendingResizeSize = contentRect ? sizeFromRect(contentRect) : readOperationSize()
|
|
1151
|
+
if (transition?.fromHero && !phoneStatic()) {
|
|
1152
|
+
resizeHeroTransition(pendingResizeSize)
|
|
1153
|
+
updateFrameGate()
|
|
1154
|
+
return
|
|
1155
|
+
}
|
|
1143
1156
|
if (phoneStatic()) {
|
|
1144
1157
|
const nextSize = pendingResizeSize
|
|
1145
1158
|
clearResizeState()
|
|
@@ -1289,21 +1302,30 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1289
1302
|
if (queuedDensity !== undefined && (queuedDensity !== particles.length || force)) startDensityReassembly(queuedDensity, force)
|
|
1290
1303
|
}
|
|
1291
1304
|
|
|
1292
|
-
function
|
|
1305
|
+
function heroContrastStrength() {
|
|
1306
|
+
const root = document.documentElement
|
|
1307
|
+
const imageVisible = preferences.backgroundMode === 'full'
|
|
1308
|
+
? root.hasAttribute('data-prts-background-full')
|
|
1309
|
+
: preferences.backgroundMode === 'split' && root.hasAttribute('data-prts-background-conversation')
|
|
1310
|
+
if (preferences.heroContrast === false || !imageVisible) return 0
|
|
1311
|
+
return heroActive ? 1 : transition?.fromHero ? 1 - easeInOut(transition.progress) : 0
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function draw(timestamp, paintOnly = false) {
|
|
1293
1315
|
frame = undefined
|
|
1294
1316
|
if (!ambient || !canvas) return
|
|
1295
|
-
if (!canRenderFrame()) return
|
|
1317
|
+
if (!canRenderFrame(paintOnly)) return
|
|
1296
1318
|
if (geometryDirty) {
|
|
1297
1319
|
syncCanvasBackingStore()
|
|
1298
1320
|
geometryDirty = false
|
|
1299
1321
|
}
|
|
1300
|
-
if (transition) {
|
|
1322
|
+
if (transition && !paintOnly) {
|
|
1301
1323
|
transitionStartedAt ??= timestamp
|
|
1302
1324
|
transition.progress = clamp((timestamp - transitionStartedAt) / TRANSITION_MS, 0, 1)
|
|
1303
1325
|
if (transition.progress >= 1) completeTransition()
|
|
1304
1326
|
}
|
|
1305
1327
|
|
|
1306
|
-
advanceDensity(timestamp)
|
|
1328
|
+
if (!paintOnly) advanceDensity(timestamp)
|
|
1307
1329
|
const reducedMotion = motionReduced()
|
|
1308
1330
|
const staticPhone = phoneStatic()
|
|
1309
1331
|
const staticField = reducedMotion || staticPhone
|
|
@@ -1322,7 +1344,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1322
1344
|
}
|
|
1323
1345
|
const frameGeometry = { activeLayout, staticField, transitionLayout }
|
|
1324
1346
|
const { center, size: fieldSize } = activeLayout
|
|
1325
|
-
const follow = reducedMotion ? 1 : 0.035
|
|
1347
|
+
const follow = paintOnly ? 0 : reducedMotion ? 1 : 0.035
|
|
1326
1348
|
glowX += (center.x - glowX) * follow
|
|
1327
1349
|
glowY += (center.y - glowY) * follow
|
|
1328
1350
|
if (glow) glow.style.transform = 'translate3d(calc(' + glowX.toFixed(2) + 'px - 50%), calc(' + glowY.toFixed(2) + 'px - 50%), 0)'
|
|
@@ -1337,19 +1359,31 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1337
1359
|
const damping = heroPhysics ? 0.86 : 0.84
|
|
1338
1360
|
const pointerForce = heroPhysics ? 2.2 : 2.7
|
|
1339
1361
|
const lightScheme = document.documentElement.dataset.prtsScheme === 'light'
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1362
|
+
const contrast = heroContrastStrength()
|
|
1363
|
+
if (contrast > 0 && context.createRadialGradient) {
|
|
1364
|
+
const origin = heroActive ? center : transition.fromCenter
|
|
1365
|
+
const radius = (heroActive ? fieldSize : transition.fromSize) * .78
|
|
1366
|
+
const shade = lightScheme ? '248,250,252' : '7,14,22'
|
|
1367
|
+
const gradient = context.createRadialGradient(origin.x, origin.y, 0, origin.x, origin.y, radius)
|
|
1368
|
+
gradient.addColorStop(0, `rgba(${shade},${.58 * contrast})`)
|
|
1369
|
+
gradient.addColorStop(.42, `rgba(${shade},${.42 * contrast})`)
|
|
1370
|
+
gradient.addColorStop(1, `rgba(${shade},0)`)
|
|
1371
|
+
context.fillStyle = gradient
|
|
1372
|
+
context.fillRect(origin.x - radius, origin.y - radius, radius * 2, radius * 2)
|
|
1373
|
+
}
|
|
1374
|
+
const baseAlpha = (lightScheme ? 0.66 : 0.72) + .16 * contrast
|
|
1375
|
+
const radiusScale = (lightScheme ? 1.12 : 1) * (1 + .08 * contrast)
|
|
1342
1376
|
const particleBatches = typeof context.moveTo === 'function' ? new Map() : null
|
|
1343
1377
|
|
|
1344
1378
|
for (const particle of particles) {
|
|
1345
1379
|
const target = targetPosition(particle, frameGeometry)
|
|
1346
|
-
if (staticField) {
|
|
1380
|
+
if (staticField && !paintOnly) {
|
|
1347
1381
|
particle.x = target.x
|
|
1348
1382
|
particle.y = target.y
|
|
1349
1383
|
particle.vx = 0
|
|
1350
1384
|
particle.vy = 0
|
|
1351
1385
|
}
|
|
1352
|
-
if (!staticField) {
|
|
1386
|
+
if (!staticField && !paintOnly) {
|
|
1353
1387
|
particle.vx += (target.x - particle.x) * spring
|
|
1354
1388
|
particle.vy += (target.y - particle.y) * spring
|
|
1355
1389
|
|
|
@@ -1412,6 +1446,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1412
1446
|
canvas.setAttribute('data-prts-particle-ready', '')
|
|
1413
1447
|
canvas.style.visibility = ''
|
|
1414
1448
|
}
|
|
1449
|
+
if (paintOnly) return
|
|
1415
1450
|
const explicitMotion = Boolean(densityTransition || transition)
|
|
1416
1451
|
const animatedField = !staticField && context && particles.length
|
|
1417
1452
|
if (explicitMotion) {
|
|
@@ -1550,7 +1585,15 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1550
1585
|
}
|
|
1551
1586
|
transitionStartedAt = undefined
|
|
1552
1587
|
pointerActive = false
|
|
1553
|
-
|
|
1588
|
+
// The initial message may have paused hydration before syncHeroState ran.
|
|
1589
|
+
// Let this short exit finish uninterrupted by subsequent response updates.
|
|
1590
|
+
cancelHydrationResume()
|
|
1591
|
+
hydrationPaused = false
|
|
1592
|
+
mutationBurstScore = 0
|
|
1593
|
+
if (mutationBurstTimer !== undefined) window.clearTimeout?.(mutationBurstTimer)
|
|
1594
|
+
mutationBurstTimer = undefined
|
|
1595
|
+
if (pendingResizeSize) resizeHeroTransition(pendingResizeSize)
|
|
1596
|
+
updateFrameGate()
|
|
1554
1597
|
}
|
|
1555
1598
|
|
|
1556
1599
|
function syncHeroState() {
|
|
@@ -1612,7 +1655,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1612
1655
|
}
|
|
1613
1656
|
|
|
1614
1657
|
function registerContentMutations(mutations) {
|
|
1615
|
-
if (!fieldEnabled() || phoneStatic()) return
|
|
1658
|
+
if (!fieldEnabled() || phoneStatic() || transition?.fromHero) return
|
|
1616
1659
|
let weight = mutations.length
|
|
1617
1660
|
for (const mutation of mutations) {
|
|
1618
1661
|
for (const node of [...mutation.addedNodes, ...mutation.removedNodes]) {
|
|
@@ -1780,6 +1823,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1780
1823
|
if (context && particles.length) {
|
|
1781
1824
|
syncCanvasBackingStore()
|
|
1782
1825
|
geometryDirty = false
|
|
1826
|
+
cancelFrame()
|
|
1783
1827
|
draw(Number(window.performance?.now?.()) || 0)
|
|
1784
1828
|
} else {
|
|
1785
1829
|
canvas.setAttribute('data-prts-particle-ready', '')
|
|
@@ -1847,6 +1891,11 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1847
1891
|
startupPaused = next
|
|
1848
1892
|
updateFrameGate()
|
|
1849
1893
|
},
|
|
1894
|
+
repaintScheme() {
|
|
1895
|
+
cancelFrame()
|
|
1896
|
+
draw(Number(window.performance?.now?.()) || 0, true)
|
|
1897
|
+
if (!schemeTransitionPaused) scheduleFrame()
|
|
1898
|
+
},
|
|
1850
1899
|
setSchemeTransitionActive(active) {
|
|
1851
1900
|
const next = Boolean(active)
|
|
1852
1901
|
if (schemeTransitionPaused === next) return
|
|
@@ -1888,10 +1937,13 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1888
1937
|
|
|
1889
1938
|
findMount()
|
|
1890
1939
|
if (!fieldEnabled()) resetTraversal()
|
|
1891
|
-
const
|
|
1940
|
+
const previousRequestedDensity = heroActive
|
|
1892
1941
|
? previousPreferences.heroParticleDensity
|
|
1893
1942
|
: previousPreferences.conversationParticleDensity
|
|
1894
|
-
|
|
1943
|
+
// Compare effective densities on both sides: sparse/light hero settings
|
|
1944
|
+
// already render at standard density, including after a focus refresh.
|
|
1945
|
+
const previousActiveDensity = heroActive ? effectiveHeroDensity(previousRequestedDensity) : previousRequestedDensity
|
|
1946
|
+
const activeDensityChanged = previousRequestedDensity !== undefined && previousActiveDensity !== activeDensity()
|
|
1895
1947
|
reportState({ requestedPattern: 'orthogonal' })
|
|
1896
1948
|
if (particles.length && activeDensityChanged) scheduleDensityReassembly(targetParticleCount(), true)
|
|
1897
1949
|
if (preservedTraversalCoordinate !== undefined && scroller && !heroActive) reanchorTraversal(preservedTraversalCoordinate)
|
|
@@ -1929,6 +1981,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1929
1981
|
enabled: fieldEnabled(),
|
|
1930
1982
|
mounted: Boolean(ambient?.isConnected && canvas?.isConnected),
|
|
1931
1983
|
hero: heroActive,
|
|
1984
|
+
heroContrast: heroContrastStrength(),
|
|
1932
1985
|
side,
|
|
1933
1986
|
emblemKey: (heroActive || transition?.fromHero ? heroEmblem() : emblemForSegment(segment))?.key,
|
|
1934
1987
|
nextEmblemKey: transition ? emblemForSegment(transition.targetSegment)?.key : null,
|
|
@@ -27,6 +27,7 @@ export const DEFAULT_PREFERENCES = Object.freeze({
|
|
|
27
27
|
composerGlass: false,
|
|
28
28
|
backgroundMode: 'off',
|
|
29
29
|
backgroundDim: .45,
|
|
30
|
+
heroContrast: true,
|
|
30
31
|
sidebarOpacity: .06,
|
|
31
32
|
railOpacity: .3,
|
|
32
33
|
bakerOpacity: .5,
|
|
@@ -99,7 +100,7 @@ export const PARTICLE_DETAIL_LEVELS = Object.freeze({
|
|
|
99
100
|
precise: Object.freeze({ conversationParticleDensity: 'dense', heroParticleDensity: 'ultra' }),
|
|
100
101
|
})
|
|
101
102
|
export const PREFERENCE_GROUPS = Object.freeze({
|
|
102
|
-
background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'sidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
|
|
103
|
+
background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'heroContrast', 'sidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
|
|
103
104
|
particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
|
|
104
105
|
material: Object.freeze(['glass', 'composerGlass']),
|
|
105
106
|
accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
|
|
@@ -220,6 +221,7 @@ export function normalizePreferences(value) {
|
|
|
220
221
|
railBlur: materialBlurOr(input.railBlur, DEFAULT_PREFERENCES.railBlur),
|
|
221
222
|
bakerBlur: materialBlurOr(input.bakerBlur, DEFAULT_PREFERENCES.bakerBlur),
|
|
222
223
|
classicUserBlur: materialBlurOr(input.classicUserBlur, DEFAULT_PREFERENCES.classicUserBlur),
|
|
224
|
+
heroContrast: booleanOr(input.heroContrast, DEFAULT_PREFERENCES.heroContrast),
|
|
223
225
|
backgroundDim: typeof input.backgroundDim === 'number' && Number.isFinite(input.backgroundDim) ? Math.min(.9, Math.max(0, input.backgroundDim)) : .45,
|
|
224
226
|
composerGlass: booleanOr(input.composerGlass, DEFAULT_PREFERENCES.composerGlass),
|
|
225
227
|
bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
|