@ash-qw/dsh-theme-prts 2.5.3 → 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-qw/dsh-theme-prts",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
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",
@@ -5,7 +5,16 @@ export function createBackgroundLayout({window,document}) {
5
5
  const raf=window.requestAnimationFrame?.bind(window) ?? (fn=>window.setTimeout(fn,16))
6
6
  const caf=window.cancelAnimationFrame?.bind(window) ?? window.clearTimeout.bind(window)
7
7
  let state={}, frame, disposed=false, enabled=false, observing=false, hostFrame, operation, sessions
8
- const set=(name,value)=>{if(root.style.getPropertyValue(name)!==value) root.style.setProperty(name,value)}
8
+ const sidebarTargets = new Set()
9
+ const set=(name,value)=>{
10
+ const targets=name.startsWith('--prts-protected-sidebar-')?[sessions,document.querySelector('[data-prts-nav-rail]')]:[root]
11
+ for(const node of targets){
12
+ if(!node)continue
13
+ if(node!==root)sidebarTargets.add(node)
14
+ if(value===null)node.style.removeProperty(name)
15
+ else if(node.style.getPropertyValue(name)!==value)node.style.setProperty(name,value)
16
+ }
17
+ }
9
18
  const update=()=>{
10
19
  frame=undefined
11
20
  if(disposed||!observing)return
@@ -17,6 +26,8 @@ export function createBackgroundLayout({window,document}) {
17
26
  for(const node of [hostFrame||operation,sessions])if(node)resize?.observe(node)
18
27
  }
19
28
  const railWidth=parseFloat(window.getComputedStyle(root).getPropertyValue('--prts-nav-width'))||0
29
+ const writes=[]
30
+ const write=(...args)=>writes.push(args)
20
31
  for(const slot of ['full','conversation','sidebar']){
21
32
  const entry=state[slot]
22
33
  const node=slot==='sidebar'?sessions:slot==='conversation'?(hostFrame||operation):operation
@@ -25,15 +36,16 @@ export function createBackgroundLayout({window,document}) {
25
36
  const active=entry?.region && width>0 && height>0
26
37
  if(active){
27
38
  const p=backgroundPlacement(entry.width,entry.height,width,height,entry.region)
28
- set(`--prts-protected-${slot}-x`,`${p.x}px`)
29
- set(`--prts-protected-${slot}-y`,`${p.y}px`)
30
- if(slot==='sidebar')set('--prts-protected-sidebar-width',`${p.width}px`)
39
+ write(`--prts-protected-${slot}-x`,`${p.x}px`)
40
+ write(`--prts-protected-${slot}-y`,`${p.y}px`)
41
+ if(slot==='sidebar')write('--prts-protected-sidebar-width',`${p.width}px`)
31
42
  }else{
32
- root.style.removeProperty(`--prts-protected-${slot}-x`)
33
- root.style.removeProperty(`--prts-protected-${slot}-y`)
34
- if(slot==='sidebar')root.style.removeProperty('--prts-protected-sidebar-width')
43
+ write(`--prts-protected-${slot}-x`,null)
44
+ write(`--prts-protected-${slot}-y`,null)
45
+ if(slot==='sidebar')write('--prts-protected-sidebar-width',null)
35
46
  }
36
47
  }
48
+ for(const args of writes)set(...args)
37
49
  }
38
50
  function schedule(){if(!disposed && observing && frame===undefined)frame=raf(update)}
39
51
  // ResizeObserver runs after layout, before paint. Deferring again to rAF
@@ -48,6 +60,8 @@ export function createBackgroundLayout({window,document}) {
48
60
  const attributes=new window.MutationObserver(schedule)
49
61
  function clear(){
50
62
  caf(frame);frame=undefined;resize?.disconnect();mutations.disconnect();attributes.disconnect();window.removeEventListener('resize',schedule)
63
+ for(const node of sidebarTargets)for(const axis of ['x','y','width'])node.style.removeProperty(`--prts-protected-sidebar-${axis}`)
64
+ sidebarTargets.clear()
51
65
  hostFrame=undefined;operation=undefined;sessions=undefined;observing=false
52
66
  for(const slot of ['full','conversation','sidebar'])for(const axis of ['x','y','width'])root.style.removeProperty(`--prts-protected-${slot}-${axis}`)
53
67
  }
@@ -49,6 +49,8 @@ function classify(button, operation) {
49
49
  if (approval) return hasClassSuffix(button, '_reject') ? 'danger' : 'cta'
50
50
  if (hasClassSuffix(button, '_modalAction') || button.closest('[role="dialog"]')) return 'cta'
51
51
 
52
+ const userRow = closestWithClassSuffix(button, '_userRow')
53
+ if (userRow?.matches('[data-submission-echo], [data-pending-steering]')) return 'micro'
52
54
  if (button.closest('[data-chat-flow-kind], [data-message-role]')) return 'micro'
53
55
  return null
54
56
  }
@@ -129,7 +131,7 @@ export function createConversationControlAdapter({ document, window }) {
129
131
  subtree: true,
130
132
  childList: true,
131
133
  attributes: true,
132
- attributeFilter: ['class', 'data-approval-key', 'data-turn-process'],
134
+ attributeFilter: ['class', 'data-approval-key', 'data-turn-process', 'data-submission-echo', 'data-pending-steering', 'data-chat-flow-kind', 'data-message-role'],
133
135
  })
134
136
  },
135
137
  dispose() {
@@ -166,7 +168,15 @@ function mountBodyTargetFrame(document, window, operation) {
166
168
  }
167
169
  let target=null, hovered=null, focused=null, raf=0, pressed=false, releasePending=false, disposed=false
168
170
  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
169
- const resizeObserver=window.ResizeObserver?new window.ResizeObserver(()=>{boundsDirty=true;wake()}):null
171
+ let moveRaf=0, pendingMove=null
172
+ const resizeObserver=window.ResizeObserver?new window.ResizeObserver(entries=>{
173
+ if(!target)return
174
+ const entry=entries.find(item=>item.target===target),size=entry?.borderBoxSize?.[0]
175
+ if(size && bounds && Math.abs(size.inlineSize-bounds.width)<.5 && Math.abs(size.blockSize-bounds.height)<.5)return
176
+ const box=target.getBoundingClientRect()
177
+ if(bounds && ['left','top','width','height'].every(key=>bounds[key]===box[key]))return
178
+ boundsDirty=true;wake()
179
+ }):null
170
180
  const reduced=window.matchMedia?.('(prefers-reduced-motion: reduce)')
171
181
  const fine=window.matchMedia?.('(hover:hover) and (pointer:fine)')
172
182
  let activeModal=null
@@ -179,11 +189,12 @@ function mountBodyTargetFrame(document, window, operation) {
179
189
  const candidate=card || pendingAction || node.closest(selector)
180
190
  return candidate && !candidate.matches(':disabled,[aria-disabled="true"]') && !candidate.closest('[inert]') ? candidate : null
181
191
  }
182
- const measure=()=>{bounds=target.getBoundingClientRect();targetArm=7+Math.max(0,Math.min(1,(Math.min(bounds.width,bounds.height)-36)/36))*5;boundsDirty=false}
192
+ 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}
183
193
  const wake=()=>{if(!raf && !disposed)raf=window.requestAnimationFrame(draw)}
184
194
  const choose=()=>{
185
195
  const next=(hovered?.isConnected?hovered:null) || (focused?.isConnected?focused:null)
186
196
  if(next!==target){
197
+ const nextBounds=next?.getBoundingClientRect()
187
198
  const retire=!next && target && !target.isConnected && targetRetires && releasePending && bounds && !reduced?.matches
188
199
  imageRetiring=false
189
200
  retiringBounds=retire?{left:bounds.left,top:bounds.top,width:bounds.width,height:bounds.height}:null
@@ -195,12 +206,13 @@ function mountBodyTargetFrame(document, window, operation) {
195
206
  if(target){
196
207
  targetRetires=hasClassSuffix(target,'_toBottom') || isPendingInteractionAction(target)
197
208
  target.setAttribute('data-prts-corner-target','');showFrame()
198
- measure();const box=bounds
199
- if(!state || state.alpha<.01)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}
200
- const style=window.getComputedStyle(target)
201
- frame.style.color=style.getPropertyValue(target.getAttribute('data-prts-conversation-control')==='danger'?'--prts-red':'--prts-yellow')
209
+ measure(nextBounds);const box=bounds
210
+ 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}
211
+ frame.style.opacity='0'
212
+ frame.style.color=target.getAttribute('data-prts-conversation-control')==='danger'?window.getComputedStyle(target).getPropertyValue('--prts-red'):'var(--prts-corner-color, var(--prts-yellow))'
202
213
  }
203
214
  }
215
+ if(!target && !state?.alpha)hideFrame()
204
216
  if(!settled || !target && state?.alpha>0)wake()
205
217
  }
206
218
  function draw(time) {
@@ -227,7 +239,7 @@ function mountBodyTargetFrame(document, window, operation) {
227
239
  tween={from:{...state},to:{...goal},start:time,duration:remaining,pressed}
228
240
  }
229
241
  // All channels share one endpoint; no separate thresholds or late position snap.
230
- const progress=reduced?.matches || settled || !tween.duration?1:Math.min(1,(time-tween.start)/tween.duration)
242
+ const progress=reduced?.matches || settled || !tween.duration || (!state.alpha && !goal.alpha)?1:Math.min(1,(time-tween.start)/tween.duration)
231
243
  const ease=1-(1-progress)**3
232
244
  for(const key of keys)state[key]=tween.from[key]+(goal[key]-tween.from[key])*ease
233
245
  const moving=progress<1
@@ -251,17 +263,28 @@ function mountBodyTargetFrame(document, window, operation) {
251
263
  else if(moving)wake()
252
264
  else {tween=null;if(!state.alpha)hideFrame()}
253
265
  }
254
- const move=event=>{
266
+ const applyMove=event=>{
255
267
  if(document.hidden || event.pointerType==='touch' || !fine?.matches)return
256
268
  pointer={x:event.clientX,y:event.clientY}
257
269
  const next=find(event.target)
258
270
  if(next===hovered && (next || !state?.alpha))return
259
271
  hovered=next;choose()
260
272
  }
261
- const leave=()=>{hovered=null;choose()}
273
+ const clearMove=()=>{window.cancelAnimationFrame(moveRaf);moveRaf=0;pendingMove=null}
274
+ const flushMove=()=>{
275
+ const event=pendingMove;clearMove()
276
+ if(event)applyMove(event)
277
+ }
278
+ const move=event=>{
279
+ if(document.hidden || event.pointerType==='touch' || !fine?.matches)return
280
+ pendingMove={target:event.target,clientX:event.clientX,clientY:event.clientY,pointerType:event.pointerType}
281
+ if(!moveRaf)moveRaf=window.requestAnimationFrame(flushMove)
282
+ }
283
+ const leave=()=>{clearMove();hovered=null;choose()}
262
284
  const focus=event=>{focused=event.target.matches?.(':focus-visible')?find(event.target):null;choose()}
263
285
  const blur=()=>{focused=null;choose()}
264
286
  const down=event=>{
287
+ flushMove()
265
288
  pointer={x:event.clientX,y:event.clientY}
266
289
  if(event.pointerType==='touch')hovered=find(event.target)
267
290
  choose()
@@ -275,6 +298,7 @@ function mountBodyTargetFrame(document, window, operation) {
275
298
  }
276
299
  const up=event=>{release();if(event.pointerType==='touch')hovered=null;choose()}
277
300
  const reset=()=>{
301
+ clearMove()
278
302
  target?.removeAttribute('data-prts-corner-target');hideFrame()
279
303
  hovered=focused=target=null;state=tween=bounds=exitGoal=retiringBounds=null
280
304
  imageRetiring=false
@@ -317,6 +341,7 @@ function mountBodyTargetFrame(document, window, operation) {
317
341
  ]
318
342
  for(const [node,event,handler] of listeners)node.addEventListener(event,handler)
319
343
  return ()=>{
344
+ clearMove()
320
345
  modalObserver?.disconnect();document.removeEventListener('toggle',modalToggle,true)
321
346
  disposed=true;target?.removeAttribute('data-prts-corner-target');hideFrame();window.cancelAnimationFrame(raf);resizeObserver?.disconnect()
322
347
  document.removeEventListener('scroll',scroll,true);window.removeEventListener('resize',reset)
@@ -8,7 +8,8 @@ export function createCursorAdapter({ document, window, assets }) {
8
8
  let gestureAssets = assets
9
9
  let textTarget
10
10
  let lastPoint
11
- const contentSelector = '[data-markdown], [data-slot*="markdown" i], [class*="markdown" i], [data-chat-flow-kind="user"] [class$="_bubble"]'
11
+ const heroTextSelector = '[data-phase="hero"] [class$="_titleGroup"]'
12
+ 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"]'
12
13
  const controlSelector = 'a[href], button, summary, input, textarea, select, [contenteditable], [role="button"], [role="toolbar"], [role="tab"]'
13
14
  function clearTextHit() {
14
15
  textTarget?.removeAttribute('data-prts-text-hit')
@@ -25,8 +26,8 @@ export function createCursorAdapter({ document, window, assets }) {
25
26
  if (event.pointerType !== 'mouse') return
26
27
  lastPoint = { clientX: event.clientX, clientY: event.clientY, pointerType: 'mouse' }
27
28
  const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
28
- const content = target?.closest?.(contentSelector)
29
- if (!content?.closest('[data-chat-flow-kind], [data-message-role]') || target.closest(controlSelector)) return
29
+ const content = target?.closest?.(`${contentSelector}, ${heroTextSelector}`)
30
+ if (!content || (!content.matches(heroTextSelector) && !content.closest('[data-chat-flow-kind], [data-message-role], [data-submission-echo], [data-pending-steering]')) || target.closest(controlSelector)) return
30
31
  const style = window.getComputedStyle(target)
31
32
  if (style.userSelect === 'none' || !style.cursor.includes('data:image/png')) return
32
33
  // Caret APIs return the nearest insertion point even over empty space.
@@ -17,7 +17,7 @@ export function createHostGeometryAdapter({ document, window }) {
17
17
  const root = document?.documentElement
18
18
  let frame
19
19
  let sidebar
20
- let previousSidebarWidth
20
+ const sidebarWidths = new Map()
21
21
  let resizeObserver
22
22
  let frameRequest
23
23
  let resizeSettleTimer
@@ -35,7 +35,11 @@ export function createHostGeometryAdapter({ document, window }) {
35
35
  }
36
36
  if (sidebar?.isConnected) {
37
37
  const width = cssPixels(sidebar.getBoundingClientRect().width)
38
- if (root.style.getPropertyValue('--prts-sidebar-width') !== width) root.style.setProperty('--prts-sidebar-width', width)
38
+ for (const node of [sidebar, document.querySelector('[data-prts-nav-rail]')]) {
39
+ if (!node) continue
40
+ if (!sidebarWidths.has(node)) sidebarWidths.set(node, [node.style.getPropertyValue('--prts-sidebar-width'), node.style.getPropertyPriority('--prts-sidebar-width')])
41
+ if (node.style.getPropertyValue('--prts-sidebar-width') !== width) node.style.setProperty('--prts-sidebar-width', width)
42
+ }
39
43
  }
40
44
  return top
41
45
  }
@@ -103,8 +107,11 @@ export function createHostGeometryAdapter({ document, window }) {
103
107
  else root.style.removeProperty(TOP_INSET_PROPERTY)
104
108
  previousValue = undefined
105
109
  previousPriority = undefined
106
- if (previousSidebarWidth) root.style.setProperty('--prts-sidebar-width', previousSidebarWidth)
107
- else root.style.removeProperty('--prts-sidebar-width')
110
+ for (const [node, [value, priority]] of sidebarWidths) {
111
+ if (value) node.style.setProperty('--prts-sidebar-width', value, priority)
112
+ else node.style.removeProperty('--prts-sidebar-width')
113
+ }
114
+ sidebarWidths.clear()
108
115
  sidebar = undefined
109
116
  frame = undefined
110
117
  observedFrameHeight = undefined
@@ -121,7 +128,6 @@ export function createHostGeometryAdapter({ document, window }) {
121
128
  if (started) dispose()
122
129
  frame = nextFrame
123
130
  sidebar = frame.querySelector?.('[data-prts-region="sessions"]')
124
- previousSidebarWidth = root.style.getPropertyValue('--prts-sidebar-width')
125
131
  previousValue = root.style.getPropertyValue(TOP_INSET_PROPERTY)
126
132
  previousPriority = root.style.getPropertyPriority(TOP_INSET_PROPERTY)
127
133
  started = true
@@ -223,6 +223,7 @@ export function applyPrtsPlugin(ctx, environment) {
223
223
  persistAndApply(updatePreferenceValue(preferences, key, value))
224
224
  },
225
225
  onPreferencePreview(key, value) {
226
+ if (key === 'cornerColor') { document.documentElement.style.setProperty('--prts-corner-color', String(value)); return }
226
227
  if (key === 'particleTraversalSpeed') {
227
228
  particleField.update(updatePreferenceValue(preferences, key, value))
228
229
  return
@@ -39,6 +39,8 @@ export const DEFAULT_PREFERENCES = Object.freeze({
39
39
  bakerBlur: 8,
40
40
  classicUserBlur: 8,
41
41
  cursor: 'prts',
42
+ cornerColor: '#f0c800',
43
+ cornerCustomColor: '#f0c800',
42
44
  conversationParticleDensity: 'sparse',
43
45
  heroParticleDensity: 'light',
44
46
  particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
@@ -105,7 +107,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
105
107
  background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'heroContrast', 'sidebarOpacity', 'rightSidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'rightSidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
106
108
  particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
107
109
  material: Object.freeze(['glass', 'composerGlass']),
108
- accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
110
+ accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor', 'cornerColor', 'cornerCustomColor']),
109
111
  aiTools: Object.freeze(['aiToolsEnabled']),
110
112
  navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewTools', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
111
113
  conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'bakerLoading', 'conversationEmblem']),
@@ -229,6 +231,8 @@ export function normalizePreferences(value) {
229
231
  backgroundDim: typeof input.backgroundDim === 'number' && Number.isFinite(input.backgroundDim) ? Math.min(.9, Math.max(0, input.backgroundDim)) : .45,
230
232
  composerGlass: booleanOr(input.composerGlass, DEFAULT_PREFERENCES.composerGlass),
231
233
  bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
234
+ cornerCustomColor: colorOr(input.cornerCustomColor, DEFAULT_PREFERENCES.cornerCustomColor),
235
+ cornerColor: colorOr(input.cornerColor, DEFAULT_PREFERENCES.cornerColor),
232
236
  cursor: input.cursor === 'system' ? 'system' : DEFAULT_PREFERENCES.cursor,
233
237
  conversationParticleDensity: enumOr(legacyDensities.conversation, PARTICLE_DENSITIES, particlePreset.conversationParticleDensity),
234
238
  heroParticleDensity: enumOr(legacyDensities.hero, PARTICLE_DENSITIES, particlePreset.heroParticleDensity),
@@ -283,6 +287,10 @@ export function updatePreferenceValue(value, key, nextValue) {
283
287
  if (key === 'preset') return applyVisualPreset(value, nextValue)
284
288
  if (key === 'particleDetail') return applyParticleDetail(value, nextValue)
285
289
  const current = normalizePreferences(value)
290
+ if (key === 'cornerCustomColor') {
291
+ const color = colorOr(nextValue, current.cornerCustomColor)
292
+ return normalizePreferences({ ...current, cornerColor: color, cornerCustomColor: color })
293
+ }
286
294
  if (key === 'sessionFlowPalette') {
287
295
  const palette = enumOr(nextValue, SESSION_FLOW_PALETTE_IDS, current.sessionFlowPalette)
288
296
  const colors = SESSION_FLOW_PALETTES[palette]
@@ -862,6 +862,28 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
862
862
  }
863
863
 
864
864
 
865
+ let pickupRow, pickupTimer, pickupRoot
866
+ function clearPickup() {
867
+ if (pickupTimer !== undefined) window.clearTimeout(pickupTimer)
868
+ pickupTimer = undefined
869
+ pickupRow?.removeAttribute('data-prts-pickup-ready')
870
+ pickupRow = undefined
871
+ }
872
+ function pickupOver(event) {
873
+ const row = event.target.closest?.('[data-prts-session-row]')
874
+ if (row === pickupRow) return
875
+ clearPickup()
876
+ if (!row) return
877
+ pickupRow = row
878
+ pickupTimer = window.setTimeout(() => {
879
+ pickupTimer = undefined
880
+ if (row.isConnected && pickupRow === row) row.setAttribute('data-prts-pickup-ready', '')
881
+ }, 100)
882
+ }
883
+ function pickupOut(event) {
884
+ if (pickupRow && !pickupRow.contains(event.relatedTarget)) clearPickup()
885
+ }
886
+
865
887
  function queueScan() {
866
888
  if (!adapterActive || scanFrame !== undefined) return
867
889
  const requestFrame = window?.requestAnimationFrame?.bind(window) ?? (callback => window?.setTimeout?.(callback, 0))
@@ -1021,8 +1043,18 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
1021
1043
  if (observer || !window?.MutationObserver) return
1022
1044
  const observationRoot = document.querySelector('[data-prts-region="sessions"]')
1023
1045
  if (!observationRoot) return
1046
+ pickupRoot = observationRoot
1047
+ pickupRoot.addEventListener('pointerover', pickupOver)
1048
+ pickupRoot.addEventListener('pointerout', pickupOut)
1024
1049
  observer = new window.MutationObserver(mutations => {
1025
- const relevant = mutations.filter(mutation => !mutation.target.closest?.('[data-prts-owned-session-summary]'))
1050
+ const relevant = mutations.filter(mutation => {
1051
+ if (mutation.target.closest?.('[data-prts-owned-session-summary]')) return false
1052
+ if (mutation.type === 'attributes' && mutation.attributeName === 'class' && !mutation.target.closest('[role="treeitem"]')) {
1053
+ const stableClasses = value => (value ?? '').split(/\s+/).filter(token => token && !token.endsWith('_quietBars')).sort().join(' ')
1054
+ if (stableClasses(mutation.oldValue) === stableClasses(mutation.target.getAttribute('class'))) return false
1055
+ }
1056
+ return true
1057
+ })
1026
1058
  const freshRow = 'div[role="treeitem"]:not([data-prts-session-row]):not([data-prts-workspace-row])'
1027
1059
  const mountsRows = relevant.some(mutation => mutation.type === 'childList'
1028
1060
  && Array.from(mutation.addedNodes).some(node => node.nodeType === 1
@@ -1040,9 +1072,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
1040
1072
  childList: true,
1041
1073
  attributes: true,
1042
1074
  attributeFilter: ['aria-expanded', 'aria-selected', 'class', 'data-state'],
1075
+ attributeOldValue: true,
1043
1076
  })
1044
1077
  },
1045
1078
  dispose() {
1079
+ clearPickup()
1080
+ pickupRoot?.removeEventListener('pointerover', pickupOver)
1081
+ pickupRoot?.removeEventListener('pointerout', pickupOut)
1082
+ pickupRoot = undefined
1046
1083
  adapterActive = false
1047
1084
  disconnectSummarySources()
1048
1085
  cancelReveal()
@@ -19,6 +19,7 @@ const ROOT_ATTRIBUTES = [
19
19
  'data-prts-background-mode',
20
20
  ]
21
21
  const SESSION_FLOW_COLOR_PROPERTIES = {
22
+ '--prts-corner-color': 'cornerColor',
22
23
  '--prts-session-flow-back': 'sessionFlowColorBack',
23
24
  '--prts-session-flow-core': 'sessionFlowColorCore',
24
25
  '--prts-session-flow-front': 'sessionFlowColorFront',
@@ -14,6 +14,7 @@ const ROOT_ATTRIBUTES = [
14
14
  'data-prts-conversation-style',
15
15
  ]
16
16
  const SESSION_FLOW_COLOR_PROPERTIES = {
17
+ '--prts-corner-color': 'cornerColor',
17
18
  '--prts-session-flow-back': 'sessionFlowColorBack',
18
19
  '--prts-session-flow-core': 'sessionFlowColorCore',
19
20
  '--prts-session-flow-front': 'sessionFlowColorFront',
@@ -171,6 +171,7 @@ export function themeSettingsMarkup() {
171
171
  <section data-prts-settings-common data-prts-settings-section="interaction" role="tabpanel" id="prts-page-interaction" aria-labelledby="prts-tab-interaction" tabindex="0" hidden>
172
172
  <div data-prts-settings-common-grid>
173
173
  ${COMMON_CONTROLS.filter(([key]) => ['cursor', 'railDefaultHidden', 'motion'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
174
+ <div data-prts-setting-row="cornerColor"><span><strong>交互提示框颜色</strong><small data-prts-setting-note>部分按钮和卡片的四角提示颜色,危险操作保持红色。</small></span><div data-prts-corner-presets role="group" aria-label="交互提示框预设颜色">${[['黄色','#f0c800'],['白色','#ffffff'],['青色','#65ddb0'],['蓝色','#6eb7f3'],['紫色','#b487ef']].map(([name,color]) => `<button type="button" data-prts-corner-preset="${color}" aria-label="${name}" title="${name}" aria-pressed="false" style="--swatch:${color}"><i aria-hidden="true"></i></button>`).join('')}<button type="button" data-prts-corner-custom title="选择自定义颜色"><i data-prts-corner-preview aria-hidden="true"></i><span>自定义</span></button><input type="color" data-prts-corner-color aria-label="交互提示框颜色" value="#f0c800" hidden></div></div>
174
175
  </div>
175
176
  </section>
176
177
  <section data-prts-settings-section="ai-tools" data-prts-ai-tools role="tabpanel" id="prts-page-ai-tools" aria-labelledby="prts-tab-ai-tools" tabindex="0" hidden>
@@ -531,6 +532,10 @@ export function createThemeSettingsOverlay({
531
532
 
532
533
  function renderState() {
533
534
  if (!panel || !currentPreferences) return
535
+ const cornerColor = panel.querySelector('[data-prts-corner-color]')
536
+ if (cornerColor) cornerColor.value = currentPreferences.cornerCustomColor
537
+ panel.querySelector('[data-prts-corner-preview]')?.style.setProperty('--swatch', currentPreferences.cornerCustomColor)
538
+ for (const preset of panel.querySelectorAll('[data-prts-corner-preset]')) preset.setAttribute('aria-pressed', String(preset.dataset.prtsCornerPreset === currentPreferences.cornerColor))
534
539
  for (const toggle of panel.querySelectorAll('[data-prts-setting-switch]')) {
535
540
  const key = toggle.dataset.prtsSettingSwitch
536
541
  const enabled = key === 'cursor' ? currentPreferences.cursor === 'prts'
@@ -625,6 +630,8 @@ export function createThemeSettingsOverlay({
625
630
  if (event.target === panel) { setOpen(false); return }
626
631
  const target = event.target?.closest?.('button')
627
632
  if (!target) return
633
+ if (target.hasAttribute('data-prts-corner-custom')) { panel.querySelector('[data-prts-corner-color]').click(); return }
634
+ if (target.hasAttribute('data-prts-corner-preset')) { onPreferenceChange('cornerColor', target.dataset.prtsCornerPreset); return }
628
635
  if (target.hasAttribute('data-prts-settings-jump')) {
629
636
  selectSection(target.dataset.prtsSettingsJump)
630
637
  return
@@ -663,6 +670,7 @@ export function createThemeSettingsOverlay({
663
670
 
664
671
  function onPanelInput(event) {
665
672
  const target = event.target
673
+ if (target?.matches?.('[data-prts-corner-color]')) { onPreferencePreview('cornerColor', target.value); return }
666
674
  if (target?.matches?.('[data-prts-preview-reply-limit]')) { onPreferenceChange('previewReplyLimit', Number(target.value)); return }
667
675
  if (target?.matches?.('input[data-prts-setting-color]')) {
668
676
  const key = target.dataset.prtsSettingKey
@@ -725,6 +733,7 @@ export function createThemeSettingsOverlay({
725
733
  const renderMediaState = () => renderState()
726
734
  const commitFromControl = event => {
727
735
  const target = event.target
736
+ if (target?.matches?.('[data-prts-corner-color]')) { if (event.type === 'change') onPreferenceChange('cornerCustomColor', target.value); return }
728
737
  if (target?.matches?.('input[data-prts-setting-color]')) {
729
738
  if (event.type === 'change' || event.type === 'focusout') commitSessionFlowColor(target.dataset.prtsSettingKey)
730
739
  return
@@ -13,9 +13,8 @@ export function themeTransferMarkup() {
13
13
  <button type="button" data-prts-transfer-export>导出主题文件 <span aria-hidden="true">↓</span></button>
14
14
  </section>
15
15
  <section data-prts-transfer-card="import" data-prts-transfer-drop>
16
- <h3>导入主题</h3><p>选择或拖入已导出的文件,确认内容后应用。</p>
17
- <div data-prts-transfer-file-hint><span aria-hidden="true">↑</span><small>.prts-theme.json · 最大 160 MB</small></div>
18
- <button type="button" data-prts-transfer-choose>选择主题文件 <span aria-hidden="true">↑</span></button>
16
+ <h3>导入主题</h3><p>导入已导出的主题文件,确认内容后应用。</p>
17
+ <button type="button" data-prts-transfer-file-hint data-prts-transfer-choose><span aria-hidden="true">↑</span><strong>点击选择或拖拽主题文件到此处</strong><small>.prts-theme.json · 最大 160 MB</small></button>
19
18
  <input type="file" accept=".prts-theme.json,application/json" aria-label="导入主题文件" data-prts-transfer-file hidden>
20
19
  </section>
21
20
  </div>
@@ -5,9 +5,10 @@ background: linear-gradient(var(--prts-background-wash), var(--prts-background-w
5
5
  }
6
6
  html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] :is([data-prts-region="sessions"], [data-prts-region="operation"]) { background: transparent; }
7
7
  html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-sidebar] {
8
+ & :is([data-prts-region="sessions"],[data-prts-nav-rail]) {
8
9
  --prts-wall-width:var(--prts-protected-sidebar-width,max(calc(var(--prts-sidebar-width,280px) + var(--prts-nav-width)),calc((100dvh - var(--prts-host-top-inset)) * var(--prts-sidebar-image-ratio,1))));
9
10
  --prts-wall-x:var(--prts-protected-sidebar-x,calc((var(--prts-sidebar-width,280px) + var(--prts-nav-width) - var(--prts-wall-width)) / 2));
10
- & :is([data-prts-region="sessions"],[data-prts-nav-rail]){background:linear-gradient(var(--prts-background-wash),var(--prts-background-wash)),var(--prts-background-sidebar) var(--prts-wall-x) var(--prts-protected-sidebar-y, 50%) / var(--prts-wall-width) auto no-repeat}
11
+ background:linear-gradient(var(--prts-background-wash),var(--prts-background-wash)),var(--prts-background-sidebar) var(--prts-wall-x) var(--prts-protected-sidebar-y, 50%) / var(--prts-wall-width) auto no-repeat}
11
12
  & [data-prts-region="sessions"]{background-position:center,calc(var(--prts-wall-x) - var(--prts-nav-width)) var(--prts-protected-sidebar-y, 50%)}
12
13
  }
13
14
  html[data-dsh-prts][data-prts-rail-mode="overlay"]:is([data-prts-background-mode="split"][data-prts-background-sidebar],[data-prts-background-mode="full"][data-prts-background-full]) [data-prts-nav-rail] { background: transparent; }
@@ -572,11 +572,11 @@ html[data-dsh-prts] [data-prts-session-pickup-bar]:nth-of-type(10) { --prts-pick
572
572
  html[data-dsh-prts] [data-prts-session-pickup-bar]:nth-of-type(11) { --prts-pickup-duration: 560ms; --prts-pickup-delay: -.3s; }
573
573
 
574
574
  @media (prefers-reduced-motion: no-preference) {
575
- html[data-dsh-prts]:not([data-prts-motion="reduced"]) [data-prts-session-row]:hover [data-prts-session-pickup-indicator] {
575
+ html[data-dsh-prts]:not([data-prts-motion="reduced"]) [data-prts-session-row]:hover[data-prts-pickup-ready] [data-prts-session-pickup-indicator] {
576
576
  animation: prts-session-record-indicator 1.1s ease-in-out infinite;
577
577
  }
578
578
 
579
- html[data-dsh-prts]:not([data-prts-motion="reduced"]) [data-prts-session-row]:hover [data-prts-session-pickup-bar] {
579
+ html[data-dsh-prts]:not([data-prts-motion="reduced"]) [data-prts-session-row]:hover[data-prts-pickup-ready] [data-prts-session-pickup-bar] {
580
580
  animation: prts-session-pickup-sample var(--prts-pickup-duration) ease-in-out var(--prts-pickup-delay) infinite;
581
581
  }
582
582
  }
@@ -886,3 +886,6 @@ html[data-dsh-prts] [data-prts-tool-record] > div { display:flex;align-items:bas
886
886
  html[data-dsh-prts] [data-prts-tool-record] b { flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--prts-cyan);font:11px/1.6 var(--prts-mono); }
887
887
  html[data-dsh-prts] [data-prts-tool-record] small { flex:none;color:var(--prts-muted);font-size:10px; }
888
888
  html[data-dsh-prts] [data-prts-tool-record] p { display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;margin:4px 0 0; }
889
+
890
+ /* Show the baseline immediately; only start the pickup after a deliberate hover. */
891
+ html[data-dsh-prts] [data-prts-session-row]:not([data-prts-pickup-ready]) :is([data-prts-session-pickup-bar],[data-prts-session-pickup-indicator]) { visibility:hidden; }
@@ -508,7 +508,7 @@ html[data-dsh-prts] [data-turn-trigger] {
508
508
  }
509
509
 
510
510
  /* Body interactions lock onto four corners; shell controls keep their own treatment. */
511
- html[data-dsh-prts] :is([data-chat-flow-kind],[data-message-role]) :is([data-prts-conversation-control],[data-presented-file],[data-changed-files],[data-produced-files-row] > button) {
511
+ html[data-dsh-prts] :is([data-chat-flow-kind],[data-message-role],[data-submission-echo],[data-pending-steering]) :is([data-prts-conversation-control],[data-presented-file],[data-changed-files],[data-produced-files-row] > button) {
512
512
  position:relative;
513
513
  --prts-control-outline:var(--prts-line);
514
514
  &:is(:hover,:focus-visible,:focus-within) {
@@ -967,7 +967,8 @@ html[data-dsh-prts] [data-prts-transfer] {
967
967
  & [data-prts-transfer-option] { align-items:flex-start; flex-wrap:nowrap; padding:12px 0 0; }
968
968
  & [data-prts-transfer-option] input { margin:5px 0 0; }
969
969
  & [data-prts-transfer-option] small { display:block; margin-top:4px; }
970
- & [data-prts-transfer-file-hint] { display:grid; place-items:center; flex:1; gap:6px; padding:12px; border:1px dashed var(--prts-line-strong); }
970
+ & [data-prts-transfer-card] > [data-prts-transfer-file-hint] { display:flex; flex-direction:column; justify-content:center; flex:1; gap:6px; min-height:130px; padding:16px 12px; border:1px dashed var(--prts-line-strong); text-align:center; white-space:normal; }
971
+ & [data-prts-transfer-file-hint] > strong { font-size:13px; font-weight:500; }
971
972
  & [data-prts-transfer-file-hint] > span { font-size:24px; color:var(--prts-muted); }
972
973
  & [data-prts-transfer-buttons] { display:flex; gap:8px; }
973
974
  & [data-prts-transfer-compatibility] { margin:0; padding:10px 12px; border:1px solid var(--prts-yellow); overflow-wrap:anywhere; }
@@ -1032,3 +1033,15 @@ html[data-dsh-prts] [data-prts-transfer]:not([aria-busy="true"]) [data-prts-tran
1032
1033
  }
1033
1034
  html[data-prts-motion="reduced"] [data-prts-transfer-card] { transition:none; }
1034
1035
  @media (prefers-reduced-motion:reduce) { [data-prts-transfer-card] { transition:none; } }
1036
+
1037
+ html[data-dsh-prts] [data-prts-setting-row="cornerColor"] { flex-wrap:wrap;gap:16px; }
1038
+ html[data-dsh-prts] [data-prts-corner-presets] { display:flex;flex-wrap:wrap;gap:6px;align-items:center;
1039
+ & > button { position:relative;display:grid;place-items:center;width:32px;height:32px;padding:5px;border:1px solid transparent;border-radius:4px;background:transparent; }
1040
+ & i { display:block;width:18px;height:18px;border-radius:3px;background:var(--swatch);box-shadow:inset 0 0 0 1px #8884; }
1041
+ & > button:hover { background:var(--prts-panel-raised); }
1042
+ & > button[aria-pressed="true"] { border-color:var(--prts-ink);background:var(--prts-panel-raised); }
1043
+ & > button[aria-pressed="true"]::after { content:"";position:absolute;width:8px;height:4px;border-left:2px solid #172025;border-bottom:2px solid #172025;transform:translateY(-1px) rotate(-45deg); }
1044
+ & > button:focus-visible { outline:2px solid var(--prts-ink);outline-offset:2px; }
1045
+ }
1046
+ html[data-dsh-prts] [data-prts-corner-presets] > [data-prts-corner-custom] { display:flex;align-items:center;gap:8px;width:auto;height:32px;margin-left:8px;padding:5px 10px;border:1px solid var(--prts-line-strong);border-radius:4px;background:var(--prts-panel-raised);color:var(--prts-ink);font:12px/1 var(--prts-body);white-space:nowrap; }
1047
+ html[data-dsh-prts] [data-prts-corner-presets] > [data-prts-corner-custom]:is(:hover,:focus-visible) { border-color:var(--prts-ink);background:color-mix(in srgb,var(--prts-ink) 8%,var(--prts-panel-raised)); }
@@ -407,14 +407,28 @@ const GlassSurface = ({
407
407
  useEffect(() => {
408
408
  if (!containerRef.current) return;
409
409
 
410
- const resizeObserver = new ResizeObserver(() => {
410
+ // PRTS: keep the existing map stretched with the live container while
411
+ // resizing. Rebuilding its distance field on every intermediate width
412
+ // defeats the outer composer's settled-shape update.
413
+ let timer;
414
+ let committed = '';
415
+ const commit = () => {
416
+ timer = undefined;
411
417
  const element = containerRef.current;
412
- if (element) setBoxSize(`${element.offsetWidth}x${element.offsetHeight}`);
418
+ if (!element || !element.offsetWidth || !element.offsetHeight) return;
419
+ const next = `${element.offsetWidth}x${element.offsetHeight}`;
420
+ if (next !== committed) { committed = next; setBoxSize(next); }
421
+ };
422
+ const resizeObserver = new ResizeObserver(() => {
423
+ clearTimeout(timer);
424
+ if (!committed) commit();
425
+ else timer = setTimeout(commit, 160);
413
426
  });
414
427
 
415
428
  resizeObserver.observe(containerRef.current);
416
429
 
417
430
  return () => {
431
+ clearTimeout(timer);
418
432
  resizeObserver.disconnect();
419
433
  };
420
434
  }, []);