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

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 (37) hide show
  1. package/CHANGELOG.md +10 -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 +203 -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/cursor-adapter.js +143 -34
  34. package/src/client/theme-settings-workbench.js +62 -8
  35. package/src/styles/cursor.css +5 -1
  36. package/src/styles/prts.css +7 -7
  37. package/src/styles/theme-settings-workbench.css +162 -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.5",
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
@@ -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,47 @@ 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>
118
- <section data-prts-settings-presets>
119
- <header><span><small>快速配置</small><strong>视觉预设</strong></span></header>
129
+ <section data-prts-settings-presets data-prts-settings-section="appearance" tabindex="-1">
130
+ <header><span><strong>视觉预设</strong></span></header>
120
131
  <div>${presetMarkup()}</div>
121
132
  </section>
122
133
 
123
134
  <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('')}
135
+ <header><span><strong>界面外观</strong></span></header>
136
+ <div data-prts-settings-common-grid>
137
+ ${COMMON_CONTROLS.filter(([key]) => ['texture', 'glass'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
138
+ ${settingButtons('conversationStyle', '会话主题', [['native', '原有'], ['deck-chat', '通讯链路']])}
139
+ </div>
140
+ </section>
141
+ <section data-prts-settings-common data-prts-settings-section="interaction" tabindex="-1">
142
+ <header><span><strong>交互</strong></span></header>
143
+ <div data-prts-settings-common-grid>
144
+ ${COMMON_CONTROLS.filter(([key]) => ['cursor', 'railDefaultHidden', 'motion'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
145
+ </div>
146
+ </section>
147
+ <section data-prts-settings-common data-prts-settings-section="flow" tabindex="-1">
148
+ <header><span><strong>会话流线</strong></span></header>
149
+ <div data-prts-settings-common-grid>
150
+ ${COMMON_CONTROLS.filter(([key]) => ['sessionFlow', 'sessionGlow', 'sessionFlowPalette'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
126
151
  ${sessionFlowColorMarkup()}
127
152
  <label data-prts-setting-row="sessionFlowSpeed" data-prts-setting-range-row data-prts-session-flow-speed>
128
153
  <span><strong>流线速度</strong><small data-prts-setting-note>控制当前会话流线的形变速度</small></span>
129
154
  <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
155
  </label>
131
- ${settingButtons('conversationStyle', '会话主题', [['native', '原有'], ['deck-chat', '通讯链路']])}
156
+
132
157
  </div>
133
158
  </section>
134
159
 
135
- <details data-prts-settings-advanced>
136
- <summary><span><small>精细调整</small><strong>粒子效果</strong></span><output data-prts-advanced-summary>标准 / 1×</output></summary>
160
+ <details data-prts-settings-advanced data-prts-settings-section="particles" tabindex="-1">
161
+ <summary><span><strong>粒子效果</strong></span><output data-prts-advanced-summary>标准 / 1×</output></summary>
137
162
  <div data-prts-settings-advanced-content>
138
163
  <section data-prts-particle-detail>
139
164
  <header><strong>粒子精度</strong><span>同时调整会话与新会话徽记</span></header>
@@ -163,6 +188,7 @@ export function themeSettingsMarkup() {
163
188
  </div>
164
189
  </footer>
165
190
  </div>
191
+ </div>
166
192
  </div>
167
193
  </dialog>`
168
194
  }
@@ -429,6 +455,18 @@ export function createThemeSettingsOverlay({
429
455
 
430
456
  function renderState() {
431
457
  if (!panel || !currentPreferences) return
458
+ for (const toggle of panel.querySelectorAll('[data-prts-setting-switch]')) {
459
+ const key = toggle.dataset.prtsSettingSwitch
460
+ const enabled = key === 'cursor' ? currentPreferences.cursor === 'prts'
461
+ : key === 'railDefaultHidden' ? currentPreferences[key] === true : currentPreferences[key] !== false
462
+ toggle.setAttribute('aria-checked', String(enabled))
463
+ toggle.classList.toggle('is-active', enabled)
464
+ }
465
+ for (const key of ['sessionGlow', 'sessionFlowPalette', 'sessionFlowColors', 'sessionFlowSpeed']) {
466
+ const row = panel.querySelector(`[data-prts-setting-row="${key}"]`)
467
+ if (row) row.hidden = currentPreferences.sessionFlow === false
468
+ || (key === 'sessionFlowColors' && activeSessionFlowPalette() !== 'custom')
469
+ }
432
470
  const particleDetail = resolveParticleDetail(currentPreferences)
433
471
  for (const button of panel.querySelectorAll('button[data-prts-setting-key]')) {
434
472
  const key = button.dataset.prtsSettingKey
@@ -482,7 +520,23 @@ export function createThemeSettingsOverlay({
482
520
  if (event.target === panel) { setOpen(false); return }
483
521
  const target = event.target?.closest?.('button')
484
522
  if (!target) return
523
+ if (target.hasAttribute('data-prts-settings-jump')) {
524
+ const section = panel.querySelector(`[data-prts-settings-section="${target.dataset.prtsSettingsJump}"]`)
525
+ const scroller = panel.querySelector('[data-prts-theme-settings-scroll]')
526
+ if (section && scroller) {
527
+ if (section.tagName === 'DETAILS') section.open = true
528
+ scroller.scrollTo?.({ top: target.dataset.prtsSettingsJump === 'appearance' ? 0 : scroller.scrollTop + section.getBoundingClientRect().top - scroller.getBoundingClientRect().top, behavior: 'instant' })
529
+ section.focus?.({ preventScroll: true })
530
+ }
531
+ return
532
+ }
485
533
  if (target.hasAttribute('data-prts-theme-settings-close')) { setOpen(false); return }
534
+ if (target.hasAttribute('data-prts-setting-switch')) {
535
+ const key = target.dataset.prtsSettingSwitch
536
+ const enabled = target.getAttribute('aria-checked') === 'true'
537
+ onPreferenceChange(key, key === 'cursor' ? (enabled ? 'system' : 'prts') : !enabled)
538
+ return
539
+ }
486
540
  if (target.hasAttribute('data-prts-setting-key')) {
487
541
  const raw = target.dataset.prtsSettingValue
488
542
  if (target.dataset.prtsSettingKey === 'sessionFlowPalette') {
@@ -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;
@@ -1712,7 +1712,7 @@ html[data-dsh-prts-settings] .prts-plugin-settings__copy strong {
1712
1712
  font-weight: 600;
1713
1713
  }
1714
1714
 
1715
- html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch'] {
1715
+ html[data-dsh-prts-settings] :is(.prts-plugin-settings button[role='switch'], [data-prts-setting-switch]) {
1716
1716
  position: relative;
1717
1717
  width: 42px;
1718
1718
  height: 24px;
@@ -1725,7 +1725,7 @@ html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch'] {
1725
1725
  cursor: pointer;
1726
1726
  }
1727
1727
 
1728
- html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch'] > span {
1728
+ html[data-dsh-prts-settings] :is(.prts-plugin-settings button[role='switch'], [data-prts-setting-switch]) > span {
1729
1729
  position: absolute;
1730
1730
  top: 3px;
1731
1731
  left: 3px;
@@ -1736,22 +1736,22 @@ html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch'] > span
1736
1736
  transition: transform 120ms ease;
1737
1737
  }
1738
1738
 
1739
- html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch'].is-active {
1739
+ html[data-dsh-prts-settings] :is(.prts-plugin-settings button[role='switch'], [data-prts-setting-switch]).is-active {
1740
1740
  background: var(--color-primary, #d9b800);
1741
1741
  color: #111;
1742
1742
  }
1743
1743
 
1744
- html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch'].is-active > span {
1744
+ html[data-dsh-prts-settings] :is(.prts-plugin-settings button[role='switch'], [data-prts-setting-switch]).is-active > span {
1745
1745
  transform: translateX(18px);
1746
1746
  }
1747
1747
 
1748
- html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch']:disabled {
1748
+ html[data-dsh-prts-settings] :is(.prts-plugin-settings button[role='switch'], [data-prts-setting-switch]):disabled {
1749
1749
  cursor: not-allowed;
1750
1750
  opacity: .45;
1751
1751
  }
1752
1752
 
1753
1753
  @media (prefers-reduced-motion: reduce) {
1754
- html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch'] > span {
1754
+ html[data-dsh-prts-settings] :is(.prts-plugin-settings button[role='switch'], [data-prts-setting-switch]) > span {
1755
1755
  transition: none;
1756
1756
  }
1757
1757
  }
@@ -1763,7 +1763,7 @@ html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch']:disable
1763
1763
  gap: 10px;
1764
1764
  }
1765
1765
 
1766
- html[data-dsh-prts-settings] .prts-plugin-settings button[role='switch'] {
1766
+ html[data-dsh-prts-settings] :is(.prts-plugin-settings button[role='switch'], [data-prts-setting-switch]) {
1767
1767
  align-self: flex-end;
1768
1768
  }
1769
1769
  }