@ash-qw/dsh-theme-prts 1.0.4 → 1.0.6

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/THIRD_PARTY_NOTICES.md +4 -4
  5. package/THIRD_PARTY_NOTICES.zh-CN.md +4 -4
  6. package/lib/client.js +226 -45
  7. package/package.json +1 -1
  8. package/src/assets/cursors/animation.json +83 -1
  9. package/src/assets/cursors/prts-link-hold.png +0 -0
  10. package/src/assets/cursors/prts-link-press-00.png +0 -0
  11. package/src/assets/cursors/prts-link-press-01.png +0 -0
  12. package/src/assets/cursors/prts-link-press-02.png +0 -0
  13. package/src/assets/cursors/prts-link-press-03.png +0 -0
  14. package/src/assets/cursors/prts-link-press-04.png +0 -0
  15. package/src/assets/cursors/prts-link-press-05.png +0 -0
  16. package/src/assets/cursors/prts-link-release-00.png +0 -0
  17. package/src/assets/cursors/prts-link-release-01.png +0 -0
  18. package/src/assets/cursors/prts-link-release-02.png +0 -0
  19. package/src/assets/cursors/prts-link-release-03.png +0 -0
  20. package/src/assets/cursors/prts-link-release-04.png +0 -0
  21. package/src/assets/cursors/prts-link-release-05.png +0 -0
  22. package/src/assets/cursors/prts-link-release-06.png +0 -0
  23. package/src/assets/cursors/prts-link-release-07.png +0 -0
  24. package/src/assets/cursors/prts-link-release-08.png +0 -0
  25. package/src/assets/cursors/prts-link-release-09.png +0 -0
  26. package/src/assets/cursors/prts-link-release-10.png +0 -0
  27. package/src/assets/cursors/prts-link-release-11.png +0 -0
  28. package/src/assets/cursors/prts-link.png +0 -0
  29. package/src/assets/cursors/prts-resize-ew.png +0 -0
  30. package/src/assets/cursors/prts-resize-nesw.png +0 -0
  31. package/src/assets/cursors/prts-resize-ns.png +0 -0
  32. package/src/assets/cursors/prts-resize-nwse.png +0 -0
  33. package/src/client/conversation-control-adapter.js +3 -1
  34. package/src/client/cursor-adapter.js +143 -34
  35. package/src/client/theme-settings-workbench.js +84 -7
  36. package/src/styles/cursor.css +5 -1
  37. package/src/styles/prts.css +26 -25
  38. package/src/styles/theme-settings-workbench.css +230 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-qw/dsh-theme-prts",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web",
@@ -424,5 +424,87 @@
424
424
  "file": "prts-text-click-84.png",
425
425
  "duration": 60000.0
426
426
  }
427
- ]
427
+ ],
428
+ "link": {
429
+ "hotspot": [
430
+ 7,
431
+ 7
432
+ ],
433
+ "press": [
434
+ {
435
+ "file": "prts-link-press-00.png",
436
+ "duration": 16.666666666666668
437
+ },
438
+ {
439
+ "file": "prts-link-press-01.png",
440
+ "duration": 16.666666666666668
441
+ },
442
+ {
443
+ "file": "prts-link-press-02.png",
444
+ "duration": 16.666666666666668
445
+ },
446
+ {
447
+ "file": "prts-link-press-03.png",
448
+ "duration": 16.666666666666668
449
+ },
450
+ {
451
+ "file": "prts-link-press-04.png",
452
+ "duration": 16.666666666666668
453
+ },
454
+ {
455
+ "file": "prts-link-press-05.png",
456
+ "duration": 60000.0
457
+ }
458
+ ],
459
+ "release": [
460
+ {
461
+ "file": "prts-link-release-00.png",
462
+ "duration": 16.666666666666668
463
+ },
464
+ {
465
+ "file": "prts-link-release-01.png",
466
+ "duration": 16.666666666666668
467
+ },
468
+ {
469
+ "file": "prts-link-release-02.png",
470
+ "duration": 16.666666666666668
471
+ },
472
+ {
473
+ "file": "prts-link-release-03.png",
474
+ "duration": 16.666666666666668
475
+ },
476
+ {
477
+ "file": "prts-link-release-04.png",
478
+ "duration": 16.666666666666668
479
+ },
480
+ {
481
+ "file": "prts-link-release-05.png",
482
+ "duration": 16.666666666666668
483
+ },
484
+ {
485
+ "file": "prts-link-release-06.png",
486
+ "duration": 16.666666666666668
487
+ },
488
+ {
489
+ "file": "prts-link-release-07.png",
490
+ "duration": 16.666666666666668
491
+ },
492
+ {
493
+ "file": "prts-link-release-08.png",
494
+ "duration": 16.666666666666668
495
+ },
496
+ {
497
+ "file": "prts-link-release-09.png",
498
+ "duration": 16.666666666666668
499
+ },
500
+ {
501
+ "file": "prts-link-release-10.png",
502
+ "duration": 16.666666666666668
503
+ },
504
+ {
505
+ "file": "prts-link-release-11.png",
506
+ "duration": 60000.0
507
+ }
508
+ ]
509
+ }
428
510
  }
Binary file
@@ -12,6 +12,8 @@ function closestWithClassSuffix(node, suffix) {
12
12
  }
13
13
 
14
14
  function classify(button, operation) {
15
+ // The host's process disclosure owns its transparent, full-width hit area.
16
+ if (button.hasAttribute('data-turn-process')) return null
15
17
  if (button.tagName !== 'BUTTON' || !operation.contains(button)) return null
16
18
  if (hasClassSuffix(button, '_sessionLogButton')) return 'header'
17
19
  if (button.closest('[data-produced-files-row]') || hasClassSuffix(button, '_showFolder')) return 'utility'
@@ -96,7 +98,7 @@ export function createConversationControlAdapter({ document, window }) {
96
98
  subtree: true,
97
99
  childList: true,
98
100
  attributes: true,
99
- attributeFilter: ['class', 'data-approval-key'],
101
+ attributeFilter: ['class', 'data-approval-key', 'data-turn-process'],
100
102
  })
101
103
  },
102
104
  dispose() {
@@ -4,12 +4,121 @@ export function createCursorAdapter({ document, window, assets }) {
4
4
  let listening = false
5
5
  let pointerId
6
6
  let timer
7
- let mode
8
- let pressX
9
- let pressY
10
7
  let handPhase
8
+ let gestureAssets = assets
9
+ let textTarget
10
+ let lastPoint
11
+ const contentSelector = '[data-markdown], [data-slot*="markdown" i], [class*="markdown" i], [data-chat-flow-kind="user"] [class$="_bubble"]'
12
+ const controlSelector = 'a[href], button, summary, input, textarea, select, [contenteditable], [role="button"], [role="toolbar"], [role="tab"]'
13
+ function clearTextHit() {
14
+ textTarget?.removeAttribute('data-prts-text-hit')
15
+ textTarget = undefined
16
+ }
17
+ function updateTextHit(event) {
18
+ const nextTarget = findTextHit(event)
19
+ if (nextTarget === textTarget) return
20
+ clearTextHit()
21
+ textTarget = nextTarget
22
+ textTarget?.setAttribute('data-prts-text-hit', '')
23
+ }
24
+ function findTextHit(event) {
25
+ if (event.pointerType !== 'mouse') return
26
+ lastPoint = { clientX: event.clientX, clientY: event.clientY, pointerType: 'mouse' }
27
+ 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
30
+ const style = window.getComputedStyle(target)
31
+ if (style.userSelect === 'none' || !style.cursor.includes('data:image/png')) return
32
+ // Caret APIs return the nearest insertion point even over empty space.
33
+ // Check its adjacent character rectangles before treating it as a text hit.
34
+ const caret = document.caretPositionFromPoint?.(event.clientX, event.clientY)
35
+ const fallback = caret ? undefined : document.caretRangeFromPoint?.(event.clientX, event.clientY)
36
+ const node = caret?.offsetNode ?? fallback?.startContainer
37
+ const offset = caret?.offset ?? fallback?.startOffset
38
+ if (node?.nodeType !== 3 || !content.contains(node) || node.parentElement?.closest(controlSelector)) return
39
+ if (window.getComputedStyle(node.parentElement).userSelect === 'none') return
40
+ const range = document.createRange()
41
+ for (const index of [offset - 1, offset]) {
42
+ if (index < 0 || index >= node.length) continue
43
+ range.setStart(node, index)
44
+ range.setEnd(node, index + 1)
45
+ for (const rect of range.getClientRects()) {
46
+ if (rect.width > 0 && event.clientX >= rect.left && event.clientX < rect.right && event.clientY >= rect.top && event.clientY < rect.bottom) {
47
+ return target
48
+ }
49
+ }
50
+ }
51
+ }
52
+ function refreshTextHit() {
53
+ if (!lastPoint) return
54
+ updateTextHit({ ...lastPoint, target: document.elementFromPoint(lastPoint.clientX, lastPoint.clientY) })
55
+ }
56
+ let resizeTarget
57
+ let resizeOriginal
58
+ let resizePriority
59
+ let resizeApplied
60
+ function clearResize() {
61
+ if (resizeTarget && resizeTarget.style.cursor === resizeApplied) {
62
+ if (resizeOriginal) resizeTarget.style.setProperty('cursor', resizeOriginal, resizePriority)
63
+ else resizeTarget.style.removeProperty('cursor')
64
+ }
65
+ resizeTarget = undefined
66
+ }
67
+ function updateResize(event) {
68
+ if (event.pointerType !== 'mouse' || !assets.resize) return
69
+ const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
70
+ if (target === resizeTarget && target.style.cursor === resizeApplied) return
71
+ clearResize()
72
+ if (!target) return
73
+ const cursor = window.getComputedStyle(target).cursor
74
+ const direction = { 'n-resize': 'ns', 's-resize': 'ns', 'ns-resize': 'ns', 'row-resize': 'ns',
75
+ 'e-resize': 'ew', 'w-resize': 'ew', 'ew-resize': 'ew', 'col-resize': 'ew',
76
+ 'ne-resize': 'nesw', 'sw-resize': 'nesw', 'nesw-resize': 'nesw',
77
+ 'nw-resize': 'nwse', 'se-resize': 'nwse', 'nwse-resize': 'nwse' }[cursor]
78
+ if (!direction) return
79
+ resizeTarget = target
80
+ resizeOriginal = target.style.getPropertyValue('cursor')
81
+ resizePriority = target.style.getPropertyPriority('cursor')
82
+ target.style.setProperty('cursor', `url("${assets.resize[direction]}") 16 16, ${cursor}`, resizePriority)
83
+ resizeApplied = target.style.cursor
84
+ }
11
85
  const reducedMedia = window?.matchMedia?.('(prefers-reduced-motion: reduce)')
12
- const frameProperty = '--prts-hand-frame'
86
+ let handTarget
87
+ let handCursor
88
+ let originalCursor
89
+ let originalPriority
90
+ let appliedCursor
91
+ function restoreHandTarget() {
92
+ if (handTarget && handTarget.style.cursor === appliedCursor) {
93
+ if (originalCursor) handTarget.style.setProperty('cursor', originalCursor, originalPriority)
94
+ else handTarget.style.removeProperty('cursor')
95
+ }
96
+ handTarget?.removeAttribute('data-prts-hand-active')
97
+ handTarget = undefined
98
+ }
99
+ // Cursor is inherited. Animating a large container still restyles its subtree.
100
+ // Keep those background hits static, while controls retain native animation.
101
+ const maxAnimatedDescendants = 128
102
+ function canAnimate(target) {
103
+ return target && target.getElementsByTagName('*').length <= maxAnimatedDescendants
104
+ }
105
+ function followHand(target) {
106
+ if (target === handTarget && target !== textTarget) return
107
+ restoreHandTarget()
108
+ if (!canAnimate(target) || !handCursor || target === textTarget) return
109
+ const cursor = window.getComputedStyle(target).cursor
110
+ if (!cursor.includes('data:image/png') || /(?:,\s*text|resize)$/.test(cursor)) return
111
+ handTarget = target
112
+ originalCursor = target.style.getPropertyValue('cursor')
113
+ originalPriority = target.style.getPropertyPriority('cursor')
114
+ target.setAttribute('data-prts-hand-active', '')
115
+ applyHandFrame()
116
+ }
117
+ function applyHandFrame() {
118
+ if (!handTarget) return
119
+ handTarget.style.setProperty('cursor', handCursor)
120
+ appliedCursor = handTarget.style.cursor
121
+ }
13
122
  function clearTimer() {
14
123
  if (timer !== undefined) window.clearTimeout(timer)
15
124
  timer = undefined
@@ -17,14 +126,15 @@ export function createCursorAdapter({ document, window, assets }) {
17
126
  function reset() {
18
127
  clearTimer()
19
128
  pointerId = undefined
20
- root?.style.removeProperty(frameProperty)
129
+ restoreHandTarget()
130
+ handCursor = undefined
21
131
  root?.style.removeProperty('--prts-text-frame')
22
- mode = undefined
23
132
  handPhase = undefined
24
133
  }
25
134
  function show(url) {
26
- const [x, y] = mode === 'text' ? assets.textHotspot : assets.hotspot
27
- root.style.setProperty(mode === 'text' ? '--prts-text-frame' : frameProperty, `url("${url}") ${x} ${y}`)
135
+ const [x, y] = gestureAssets.hotspot
136
+ handCursor = `url("${url}") ${x} ${y}, pointer`
137
+ applyHandFrame()
28
138
  }
29
139
  function reduced() {
30
140
  return root.dataset.prtsMotion === 'reduced' || reducedMedia?.matches
@@ -44,59 +154,58 @@ export function createCursorAdapter({ document, window, assets }) {
44
154
  }
45
155
  function down(event) {
46
156
  if (event.pointerType !== 'mouse' || event.button !== 0 || event.isPrimary === false) return
157
+ updateResize(event)
158
+ updateTextHit(event)
47
159
  const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
48
- const cursor = target ? window.getComputedStyle(target).cursor : ''
49
160
  reset()
161
+ const cursor = target ? window.getComputedStyle(target).cursor : ''
50
162
  if (!cursor.includes('data:image/png')) return
51
- mode = /,\s*text$/.test(cursor) ? 'text' : 'hand'
163
+ if (/(?:,\s*text|resize)$/.test(cursor) || !canAnimate(target)) return
164
+ gestureAssets = /,\s*pointer$/.test(cursor) && assets.link ? assets.link : assets
52
165
  pointerId = event.pointerId
53
- pressX = event.clientX
54
- pressY = event.clientY
55
- if (mode === 'text') {
56
- if (reduced() || !assets.textClick) { reset(); return }
57
- play(assets.textClick, reset)
58
- return
59
- }
60
- if (reduced()) { clearTimer(); show(assets.hold); return }
166
+ handCursor = cursor
167
+ followHand(target)
168
+ if (reduced()) { clearTimer(); show(gestureAssets.hold); return }
61
169
  handPhase = 'press'
62
- play(assets.press, () => {
170
+ play(gestureAssets.press, () => {
63
171
  handPhase = 'hold'
64
- show(assets.hold)
172
+ show(gestureAssets.hold)
65
173
  // A quick tap must still reach a visible pressed pose before recovering.
66
174
  if (pointerId === undefined) timer = window.setTimeout(releaseHand, 48)
67
175
  })
68
176
  }
69
177
  function releaseHand() {
70
178
  handPhase = 'release'
71
- play(assets.release, reset)
179
+ play(gestureAssets.release, reset)
72
180
  }
73
181
  function up(event) {
74
182
  if (pointerId === undefined || event.pointerId !== pointerId || event.button !== 0) return
75
183
  pointerId = undefined
76
- if (mode === 'text') return
77
184
  if (reduced()) { reset(); return }
78
185
  if (handPhase === 'press') return
79
186
  releaseHand()
80
187
  }
81
188
  function move(event) {
82
- if (mode === 'text') {
83
- const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
84
- const cursor = target ? window.getComputedStyle(target).cursor : ''
85
- const selecting = pointerId === event.pointerId && (event.buttons & 1) && Math.hypot(event.clientX - pressX, event.clientY - pressY) > 4
86
- if (!/,\s*text$/.test(cursor) || selecting) reset()
87
- return
88
- }
189
+ updateResize(event)
190
+ updateTextHit(event)
191
+ if (handCursor) followHand(event.target?.nodeType === 1 ? event.target : event.target?.parentElement)
89
192
  if (pointerId !== undefined && event.pointerId === pointerId && !(event.buttons & 1)) reset()
90
193
  }
91
- function leave(event) { if (!event.relatedTarget) reset() }
194
+ function resetInteraction() {
195
+ reset()
196
+ clearResize()
197
+ clearTextHit()
198
+ lastPoint = undefined
199
+ }
200
+ function leave(event) { if (!event.relatedTarget) resetInteraction() }
92
201
  function blur(event) {
93
202
  // Captured element blur also reaches window when focus changes inside the app.
94
- if (event.target === window) reset()
203
+ if (event.target === window) resetInteraction()
95
204
  }
96
- function visibility() { if (document.hidden) reset() }
97
- const events = [['pointerdown', down], ['pointerup', up], ['pointermove', move], ['pointercancel', reset], ['blur', blur], ['dragstart', reset], ['pointerout', leave]]
205
+ function visibility() { if (document.hidden) resetInteraction() }
206
+ const events = [['pointerdown', down], ['pointerup', up], ['pointermove', move], ['pointercancel', resetInteraction], ['blur', blur], ['dragstart', resetInteraction], ['pointerout', leave], ['scroll', refreshTextHit], ['resize', refreshTextHit]]
98
207
  function dispose() {
99
- reset()
208
+ resetInteraction()
100
209
  if (!listening) return
101
210
  for (const [name, handler] of events) window.removeEventListener(name, handler, true)
102
211
  document.removeEventListener('visibilitychange', visibility)
@@ -15,7 +15,7 @@ const PRESET_OPTIONS = [
15
15
  ]
16
16
 
17
17
  const COMMON_CONTROLS = [
18
- ['cursor', '鼠标样式', [['system', '系统'], ['prts', 'PRTS 手形']]],
18
+ ['cursor', 'PRTS 光标', [['system', '关闭'], ['prts', '开启']]],
19
19
  ['railDefaultHidden', '默认隐藏导航栏', [['false', '关闭'], ['true', '开启']]],
20
20
  ['texture', '环境底纹', [['off', '关闭'], ['restrained', '克制'], ['full', '完整']]],
21
21
  ['glass', '玻璃材质', [['off', '关闭'], ['soft', '柔和'], ['standard', '标准'], ['clear', '清晰']]],
@@ -53,6 +53,10 @@ function settingButtons(key, label, options) {
53
53
  : key === 'sessionFlowPalette'
54
54
  ? '<small data-prts-setting-note>同时适配日间与夜间模式</small>'
55
55
  : ''
56
+ if (['cursor', 'railDefaultHidden', 'sessionFlow', 'sessionGlow'].includes(key)) return `<div data-prts-setting-row="${key}">
57
+ <span><strong>${label}</strong>${note}</span>
58
+ <button type="button" role="switch" aria-label="${label}" aria-checked="false" data-prts-setting-switch="${key}" ${key === 'cursor' ? 'data-prts-cursor-switch' : ''}><span aria-hidden="true"></span></button>
59
+ </div>`
56
60
  const sample = value => key === 'texture' || key === 'glass' || key === 'sessionFlowPalette'
57
61
  ? `<i data-prts-option-sample="${key}" data-prts-option-sample-value="${value}" aria-hidden="true"></i>`
58
62
  : ''
@@ -114,26 +118,50 @@ export function themeSettingsMarkup() {
114
118
  <button type="button" data-prts-theme-settings-close aria-label="关闭主题设置">×</button>
115
119
  </header>
116
120
 
121
+ <div data-prts-settings-body>
122
+ <nav data-prts-settings-nav aria-label="设置分区">
123
+ <button type="button" data-prts-settings-jump="appearance">外观</button>
124
+ <button type="button" data-prts-settings-jump="interaction">交互</button>
125
+ <button type="button" data-prts-settings-jump="flow">会话流线</button>
126
+ <button type="button" data-prts-settings-jump="particles">粒子效果</button>
127
+ </nav>
117
128
  <div data-prts-theme-settings-scroll>
129
+ <section data-prts-settings-section="appearance" tabindex="-1">
130
+ <header data-prts-section-heading><strong>外观</strong><small>预设、界面材质与会话样式</small></header>
118
131
  <section data-prts-settings-presets>
119
- <header><span><small>快速配置</small><strong>视觉预设</strong></span></header>
132
+ <header><span><strong>视觉预设</strong></span></header>
120
133
  <div>${presetMarkup()}</div>
121
134
  </section>
122
135
 
123
136
  <section data-prts-settings-common>
124
- <header><span><small>常用设置</small><strong>主题表现</strong></span></header>
125
- <div data-prts-settings-common-grid>${COMMON_CONTROLS.map(([key, label, options]) => settingButtons(key, label, options)).join('')}
137
+ <header><span><strong>界面外观</strong></span></header>
138
+ <div data-prts-settings-common-grid>
139
+ ${COMMON_CONTROLS.filter(([key]) => ['texture', 'glass'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
140
+ ${settingButtons('conversationStyle', '会话主题', [['native', '原有'], ['deck-chat', '通讯链路']])}
141
+ </div>
142
+ </section>
143
+ </section>
144
+ <section data-prts-settings-common data-prts-settings-section="interaction" tabindex="-1">
145
+ <header data-prts-section-heading><strong>交互</strong><small>光标、导航栏与动态反馈</small></header>
146
+ <div data-prts-settings-common-grid>
147
+ ${COMMON_CONTROLS.filter(([key]) => ['cursor', 'railDefaultHidden', 'motion'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
148
+ </div>
149
+ </section>
150
+ <section data-prts-settings-common data-prts-settings-section="flow" tabindex="-1">
151
+ <header data-prts-section-heading><strong>会话流线</strong><small>当前会话的流线与辉光</small></header>
152
+ <div data-prts-settings-common-grid>
153
+ ${COMMON_CONTROLS.filter(([key]) => ['sessionFlow', 'sessionGlow', 'sessionFlowPalette'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
126
154
  ${sessionFlowColorMarkup()}
127
155
  <label data-prts-setting-row="sessionFlowSpeed" data-prts-setting-range-row data-prts-session-flow-speed>
128
156
  <span><strong>流线速度</strong><small data-prts-setting-note>控制当前会话流线的形变速度</small></span>
129
157
  <div><input type="range" min="0.25" max="2" step="0.25" value="1" data-prts-setting-range data-prts-setting-key="sessionFlowSpeed" aria-label="当前会话流线速度"><output data-prts-session-flow-speed-output>1× · 标准</output></div>
130
158
  </label>
131
- ${settingButtons('conversationStyle', '会话主题', [['native', '原有'], ['deck-chat', '通讯链路']])}
159
+
132
160
  </div>
133
161
  </section>
134
162
 
135
- <details data-prts-settings-advanced>
136
- <summary><span><small>精细调整</small><strong>粒子效果</strong></span><output data-prts-advanced-summary>标准 / 1×</output></summary>
163
+ <details data-prts-settings-advanced data-prts-settings-section="particles" tabindex="-1">
164
+ <summary data-prts-section-heading><span><strong>粒子效果</strong><small>徽记精度与跟随移动速度</small></span><output data-prts-advanced-summary>标准 / 1×</output></summary>
137
165
  <div data-prts-settings-advanced-content>
138
166
  <section data-prts-particle-detail>
139
167
  <header><strong>粒子精度</strong><span>同时调整会话与新会话徽记</span></header>
@@ -163,6 +191,7 @@ export function themeSettingsMarkup() {
163
191
  </div>
164
192
  </footer>
165
193
  </div>
194
+ </div>
166
195
  </div>
167
196
  </dialog>`
168
197
  }
@@ -429,6 +458,18 @@ export function createThemeSettingsOverlay({
429
458
 
430
459
  function renderState() {
431
460
  if (!panel || !currentPreferences) return
461
+ for (const toggle of panel.querySelectorAll('[data-prts-setting-switch]')) {
462
+ const key = toggle.dataset.prtsSettingSwitch
463
+ const enabled = key === 'cursor' ? currentPreferences.cursor === 'prts'
464
+ : key === 'railDefaultHidden' ? currentPreferences[key] === true : currentPreferences[key] !== false
465
+ toggle.setAttribute('aria-checked', String(enabled))
466
+ toggle.classList.toggle('is-active', enabled)
467
+ }
468
+ for (const key of ['sessionGlow', 'sessionFlowPalette', 'sessionFlowColors', 'sessionFlowSpeed']) {
469
+ const row = panel.querySelector(`[data-prts-setting-row="${key}"]`)
470
+ if (row) row.hidden = currentPreferences.sessionFlow === false
471
+ || (key === 'sessionFlowColors' && activeSessionFlowPalette() !== 'custom')
472
+ }
432
473
  const particleDetail = resolveParticleDetail(currentPreferences)
433
474
  for (const button of panel.querySelectorAll('button[data-prts-setting-key]')) {
434
475
  const key = button.dataset.prtsSettingKey
@@ -478,11 +519,44 @@ export function createThemeSettingsOverlay({
478
519
  }, 300)
479
520
  }
480
521
 
522
+ function syncSectionNavigation() {
523
+ const scroller = panel?.querySelector('[data-prts-theme-settings-scroll]')
524
+ if (!scroller || !scroller.clientHeight) return
525
+ const sections = [...panel.querySelectorAll('[data-prts-settings-section]')]
526
+ const top = scroller.getBoundingClientRect().top + 32
527
+ let current = sections[0]
528
+ for (const section of sections) {
529
+ if (section.getBoundingClientRect().top <= top) current = section
530
+ }
531
+ if (scroller.scrollTop > 0 && scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2) current = sections.at(-1)
532
+ for (const button of panel.querySelectorAll('[data-prts-settings-jump]')) {
533
+ if (button.dataset.prtsSettingsJump === current?.dataset.prtsSettingsSection) button.setAttribute('aria-current', 'location')
534
+ else button.removeAttribute('aria-current')
535
+ }
536
+ }
537
+
481
538
  function onPanelClick(event) {
482
539
  if (event.target === panel) { setOpen(false); return }
483
540
  const target = event.target?.closest?.('button')
484
541
  if (!target) return
542
+ if (target.hasAttribute('data-prts-settings-jump')) {
543
+ const section = panel.querySelector(`[data-prts-settings-section="${target.dataset.prtsSettingsJump}"]`)
544
+ const scroller = panel.querySelector('[data-prts-theme-settings-scroll]')
545
+ if (section && scroller) {
546
+ if (section.tagName === 'DETAILS') section.open = true
547
+ scroller.scrollTo?.({ top: target.dataset.prtsSettingsJump === 'appearance' ? 0 : scroller.scrollTop + section.getBoundingClientRect().top - scroller.getBoundingClientRect().top - 16, behavior: 'instant' })
548
+ section.focus?.({ preventScroll: true })
549
+ syncSectionNavigation()
550
+ }
551
+ return
552
+ }
485
553
  if (target.hasAttribute('data-prts-theme-settings-close')) { setOpen(false); return }
554
+ if (target.hasAttribute('data-prts-setting-switch')) {
555
+ const key = target.dataset.prtsSettingSwitch
556
+ const enabled = target.getAttribute('aria-checked') === 'true'
557
+ onPreferenceChange(key, key === 'cursor' ? (enabled ? 'system' : 'prts') : !enabled)
558
+ return
559
+ }
486
560
  if (target.hasAttribute('data-prts-setting-key')) {
487
561
  const raw = target.dataset.prtsSettingValue
488
562
  if (target.dataset.prtsSettingKey === 'sessionFlowPalette') {
@@ -568,6 +642,8 @@ export function createThemeSettingsOverlay({
568
642
 
569
643
  trigger?.addEventListener('click', toggle)
570
644
  panel?.addEventListener('click', onPanelClick)
645
+ panel?.querySelector('[data-prts-theme-settings-scroll]')?.addEventListener('scroll', syncSectionNavigation, { passive: true })
646
+ panel?.querySelector('[data-prts-settings-jump]')?.setAttribute('aria-current', 'location')
571
647
  panel?.addEventListener('cancel', onCancel)
572
648
  panel?.addEventListener('input', onPanelInput)
573
649
  panel?.addEventListener('change', commitFromControl)
@@ -602,6 +678,7 @@ export function createThemeSettingsOverlay({
602
678
  trigger?.setAttribute('aria-expanded', 'false')
603
679
  trigger?.removeEventListener('click', toggle)
604
680
  panel?.removeEventListener('click', onPanelClick)
681
+ panel?.querySelector('[data-prts-theme-settings-scroll]')?.removeEventListener('scroll', syncSectionNavigation)
605
682
  panel?.removeEventListener('cancel', onCancel)
606
683
  panel?.removeEventListener('input', onPanelInput)
607
684
  panel?.removeEventListener('change', commitFromControl)
@@ -3,10 +3,14 @@
3
3
  html[data-dsh-prts][data-prts-cursor="prts"] {
4
4
  --prts-hand-cursor: var(--prts-hand-frame, url("__PRTS_HAND__") 7 6);
5
5
  --prts-text-cursor: var(--prts-text-frame, url("__PRTS_TEXT__") 24 24);
6
+ --prts-link-cursor: url("__PRTS_LINK__") 7 7;
6
7
  cursor: var(--prts-hand-cursor), default;
7
8
  }
8
9
  html[data-dsh-prts][data-prts-cursor="prts"] :is(button, a[href], summary, [role="button"], [role="tab"], [role="treeitem"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [data-prts-workspace-row], [data-prts-session-row], .dsh-partner-sidebar-row, select, input[type="button"], input[type="submit"], input[type="checkbox"], input[type="radio"], input[type="color"], input[type="range"]) {
9
- cursor: var(--prts-hand-cursor), pointer;
10
+ cursor: var(--prts-link-cursor), pointer;
11
+ }
12
+ html[data-dsh-prts][data-prts-cursor="prts"] [data-prts-text-hit] {
13
+ cursor: var(--prts-text-cursor), text;
10
14
  }
11
15
  html[data-dsh-prts][data-prts-cursor="prts"] :is(textarea, input:not([type]), input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], input[type="number"], input[type="tel"], [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]) {
12
16
  cursor: var(--prts-text-cursor), text;