@ash-qw/dsh-theme-prts 1.0.3 → 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 (146) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.en.md +3 -1
  3. package/README.md +4 -2
  4. package/THIRD_PARTY_NOTICES.md +9 -12
  5. package/THIRD_PARTY_NOTICES.zh-CN.md +45 -0
  6. package/lib/client.js +291 -11
  7. package/package.json +3 -2
  8. package/src/assets/cursors/animation.json +510 -0
  9. package/src/assets/cursors/prts-hand-hold.png +0 -0
  10. package/src/assets/cursors/prts-hand-press-00.png +0 -0
  11. package/src/assets/cursors/prts-hand-press-01.png +0 -0
  12. package/src/assets/cursors/prts-hand-press-02.png +0 -0
  13. package/src/assets/cursors/prts-hand-press-03.png +0 -0
  14. package/src/assets/cursors/prts-hand-press-04.png +0 -0
  15. package/src/assets/cursors/prts-hand-press-05.png +0 -0
  16. package/src/assets/cursors/prts-hand-release-00.png +0 -0
  17. package/src/assets/cursors/prts-hand-release-01.png +0 -0
  18. package/src/assets/cursors/prts-hand-release-02.png +0 -0
  19. package/src/assets/cursors/prts-hand-release-03.png +0 -0
  20. package/src/assets/cursors/prts-hand-release-04.png +0 -0
  21. package/src/assets/cursors/prts-hand-release-05.png +0 -0
  22. package/src/assets/cursors/prts-hand-release-06.png +0 -0
  23. package/src/assets/cursors/prts-hand-release-07.png +0 -0
  24. package/src/assets/cursors/prts-hand-release-08.png +0 -0
  25. package/src/assets/cursors/prts-hand-release-09.png +0 -0
  26. package/src/assets/cursors/prts-hand-release-10.png +0 -0
  27. package/src/assets/cursors/prts-hand-release-11.png +0 -0
  28. package/src/assets/cursors/prts-hand.png +0 -0
  29. package/src/assets/cursors/prts-link-hold.png +0 -0
  30. package/src/assets/cursors/prts-link-press-00.png +0 -0
  31. package/src/assets/cursors/prts-link-press-01.png +0 -0
  32. package/src/assets/cursors/prts-link-press-02.png +0 -0
  33. package/src/assets/cursors/prts-link-press-03.png +0 -0
  34. package/src/assets/cursors/prts-link-press-04.png +0 -0
  35. package/src/assets/cursors/prts-link-press-05.png +0 -0
  36. package/src/assets/cursors/prts-link-release-00.png +0 -0
  37. package/src/assets/cursors/prts-link-release-01.png +0 -0
  38. package/src/assets/cursors/prts-link-release-02.png +0 -0
  39. package/src/assets/cursors/prts-link-release-03.png +0 -0
  40. package/src/assets/cursors/prts-link-release-04.png +0 -0
  41. package/src/assets/cursors/prts-link-release-05.png +0 -0
  42. package/src/assets/cursors/prts-link-release-06.png +0 -0
  43. package/src/assets/cursors/prts-link-release-07.png +0 -0
  44. package/src/assets/cursors/prts-link-release-08.png +0 -0
  45. package/src/assets/cursors/prts-link-release-09.png +0 -0
  46. package/src/assets/cursors/prts-link-release-10.png +0 -0
  47. package/src/assets/cursors/prts-link-release-11.png +0 -0
  48. package/src/assets/cursors/prts-link.png +0 -0
  49. package/src/assets/cursors/prts-resize-ew.png +0 -0
  50. package/src/assets/cursors/prts-resize-nesw.png +0 -0
  51. package/src/assets/cursors/prts-resize-ns.png +0 -0
  52. package/src/assets/cursors/prts-resize-nwse.png +0 -0
  53. package/src/assets/cursors/prts-text-click-00.png +0 -0
  54. package/src/assets/cursors/prts-text-click-01.png +0 -0
  55. package/src/assets/cursors/prts-text-click-02.png +0 -0
  56. package/src/assets/cursors/prts-text-click-03.png +0 -0
  57. package/src/assets/cursors/prts-text-click-04.png +0 -0
  58. package/src/assets/cursors/prts-text-click-05.png +0 -0
  59. package/src/assets/cursors/prts-text-click-06.png +0 -0
  60. package/src/assets/cursors/prts-text-click-07.png +0 -0
  61. package/src/assets/cursors/prts-text-click-08.png +0 -0
  62. package/src/assets/cursors/prts-text-click-09.png +0 -0
  63. package/src/assets/cursors/prts-text-click-10.png +0 -0
  64. package/src/assets/cursors/prts-text-click-11.png +0 -0
  65. package/src/assets/cursors/prts-text-click-12.png +0 -0
  66. package/src/assets/cursors/prts-text-click-13.png +0 -0
  67. package/src/assets/cursors/prts-text-click-14.png +0 -0
  68. package/src/assets/cursors/prts-text-click-15.png +0 -0
  69. package/src/assets/cursors/prts-text-click-16.png +0 -0
  70. package/src/assets/cursors/prts-text-click-17.png +0 -0
  71. package/src/assets/cursors/prts-text-click-18.png +0 -0
  72. package/src/assets/cursors/prts-text-click-19.png +0 -0
  73. package/src/assets/cursors/prts-text-click-20.png +0 -0
  74. package/src/assets/cursors/prts-text-click-21.png +0 -0
  75. package/src/assets/cursors/prts-text-click-22.png +0 -0
  76. package/src/assets/cursors/prts-text-click-23.png +0 -0
  77. package/src/assets/cursors/prts-text-click-24.png +0 -0
  78. package/src/assets/cursors/prts-text-click-25.png +0 -0
  79. package/src/assets/cursors/prts-text-click-26.png +0 -0
  80. package/src/assets/cursors/prts-text-click-27.png +0 -0
  81. package/src/assets/cursors/prts-text-click-28.png +0 -0
  82. package/src/assets/cursors/prts-text-click-29.png +0 -0
  83. package/src/assets/cursors/prts-text-click-30.png +0 -0
  84. package/src/assets/cursors/prts-text-click-31.png +0 -0
  85. package/src/assets/cursors/prts-text-click-32.png +0 -0
  86. package/src/assets/cursors/prts-text-click-33.png +0 -0
  87. package/src/assets/cursors/prts-text-click-34.png +0 -0
  88. package/src/assets/cursors/prts-text-click-35.png +0 -0
  89. package/src/assets/cursors/prts-text-click-36.png +0 -0
  90. package/src/assets/cursors/prts-text-click-37.png +0 -0
  91. package/src/assets/cursors/prts-text-click-38.png +0 -0
  92. package/src/assets/cursors/prts-text-click-39.png +0 -0
  93. package/src/assets/cursors/prts-text-click-40.png +0 -0
  94. package/src/assets/cursors/prts-text-click-41.png +0 -0
  95. package/src/assets/cursors/prts-text-click-42.png +0 -0
  96. package/src/assets/cursors/prts-text-click-43.png +0 -0
  97. package/src/assets/cursors/prts-text-click-44.png +0 -0
  98. package/src/assets/cursors/prts-text-click-45.png +0 -0
  99. package/src/assets/cursors/prts-text-click-46.png +0 -0
  100. package/src/assets/cursors/prts-text-click-47.png +0 -0
  101. package/src/assets/cursors/prts-text-click-48.png +0 -0
  102. package/src/assets/cursors/prts-text-click-49.png +0 -0
  103. package/src/assets/cursors/prts-text-click-50.png +0 -0
  104. package/src/assets/cursors/prts-text-click-51.png +0 -0
  105. package/src/assets/cursors/prts-text-click-52.png +0 -0
  106. package/src/assets/cursors/prts-text-click-53.png +0 -0
  107. package/src/assets/cursors/prts-text-click-54.png +0 -0
  108. package/src/assets/cursors/prts-text-click-55.png +0 -0
  109. package/src/assets/cursors/prts-text-click-56.png +0 -0
  110. package/src/assets/cursors/prts-text-click-57.png +0 -0
  111. package/src/assets/cursors/prts-text-click-58.png +0 -0
  112. package/src/assets/cursors/prts-text-click-59.png +0 -0
  113. package/src/assets/cursors/prts-text-click-60.png +0 -0
  114. package/src/assets/cursors/prts-text-click-61.png +0 -0
  115. package/src/assets/cursors/prts-text-click-62.png +0 -0
  116. package/src/assets/cursors/prts-text-click-63.png +0 -0
  117. package/src/assets/cursors/prts-text-click-64.png +0 -0
  118. package/src/assets/cursors/prts-text-click-65.png +0 -0
  119. package/src/assets/cursors/prts-text-click-66.png +0 -0
  120. package/src/assets/cursors/prts-text-click-67.png +0 -0
  121. package/src/assets/cursors/prts-text-click-68.png +0 -0
  122. package/src/assets/cursors/prts-text-click-69.png +0 -0
  123. package/src/assets/cursors/prts-text-click-70.png +0 -0
  124. package/src/assets/cursors/prts-text-click-71.png +0 -0
  125. package/src/assets/cursors/prts-text-click-72.png +0 -0
  126. package/src/assets/cursors/prts-text-click-73.png +0 -0
  127. package/src/assets/cursors/prts-text-click-74.png +0 -0
  128. package/src/assets/cursors/prts-text-click-75.png +0 -0
  129. package/src/assets/cursors/prts-text-click-76.png +0 -0
  130. package/src/assets/cursors/prts-text-click-77.png +0 -0
  131. package/src/assets/cursors/prts-text-click-78.png +0 -0
  132. package/src/assets/cursors/prts-text-click-79.png +0 -0
  133. package/src/assets/cursors/prts-text-click-80.png +0 -0
  134. package/src/assets/cursors/prts-text-click-81.png +0 -0
  135. package/src/assets/cursors/prts-text-click-82.png +0 -0
  136. package/src/assets/cursors/prts-text-click-83.png +0 -0
  137. package/src/assets/cursors/prts-text-click-84.png +0 -0
  138. package/src/assets/cursors/prts-text.png +0 -0
  139. package/src/client/cursor-adapter.js +224 -0
  140. package/src/client/index.js +1 -0
  141. package/src/client/preferences.js +3 -1
  142. package/src/client/theme-controller-v2.js +7 -1
  143. package/src/client/theme-settings-workbench.js +62 -7
  144. package/src/styles/cursor.css +21 -0
  145. package/src/styles/prts.css +7 -7
  146. package/src/styles/theme-settings-workbench.css +162 -0
Binary file
@@ -0,0 +1,224 @@
1
+ // Keep the native cursor and change its image only during a primary mouse gesture.
2
+ export function createCursorAdapter({ document, window, assets }) {
3
+ const root = document?.documentElement
4
+ let listening = false
5
+ let pointerId
6
+ let timer
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
+ }
85
+ const reducedMedia = window?.matchMedia?.('(prefers-reduced-motion: reduce)')
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
+ }
122
+ function clearTimer() {
123
+ if (timer !== undefined) window.clearTimeout(timer)
124
+ timer = undefined
125
+ }
126
+ function reset() {
127
+ clearTimer()
128
+ pointerId = undefined
129
+ restoreHandTarget()
130
+ handCursor = undefined
131
+ root?.style.removeProperty('--prts-text-frame')
132
+ handPhase = undefined
133
+ }
134
+ function show(url) {
135
+ const [x, y] = gestureAssets.hotspot
136
+ handCursor = `url("${url}") ${x} ${y}, pointer`
137
+ applyHandFrame()
138
+ }
139
+ function reduced() {
140
+ return root.dataset.prtsMotion === 'reduced' || reducedMedia?.matches
141
+ }
142
+ function play(frames, done) {
143
+ clearTimer()
144
+ let index = 0
145
+ function next() {
146
+ timer = undefined
147
+ // The source ANI ends with a 60-second hold frame. Use the static state instead.
148
+ if (index >= frames.length - 1) { done(); return }
149
+ const frame = frames[index++]
150
+ show(frame.url)
151
+ timer = window.setTimeout(next, frame.duration)
152
+ }
153
+ next()
154
+ }
155
+ function down(event) {
156
+ if (event.pointerType !== 'mouse' || event.button !== 0 || event.isPrimary === false) return
157
+ updateResize(event)
158
+ updateTextHit(event)
159
+ const target = event.target?.nodeType === 1 ? event.target : event.target?.parentElement
160
+ reset()
161
+ const cursor = target ? window.getComputedStyle(target).cursor : ''
162
+ if (!cursor.includes('data:image/png')) return
163
+ if (/(?:,\s*text|resize)$/.test(cursor) || !canAnimate(target)) return
164
+ gestureAssets = /,\s*pointer$/.test(cursor) && assets.link ? assets.link : assets
165
+ pointerId = event.pointerId
166
+ handCursor = cursor
167
+ followHand(target)
168
+ if (reduced()) { clearTimer(); show(gestureAssets.hold); return }
169
+ handPhase = 'press'
170
+ play(gestureAssets.press, () => {
171
+ handPhase = 'hold'
172
+ show(gestureAssets.hold)
173
+ // A quick tap must still reach a visible pressed pose before recovering.
174
+ if (pointerId === undefined) timer = window.setTimeout(releaseHand, 48)
175
+ })
176
+ }
177
+ function releaseHand() {
178
+ handPhase = 'release'
179
+ play(gestureAssets.release, reset)
180
+ }
181
+ function up(event) {
182
+ if (pointerId === undefined || event.pointerId !== pointerId || event.button !== 0) return
183
+ pointerId = undefined
184
+ if (reduced()) { reset(); return }
185
+ if (handPhase === 'press') return
186
+ releaseHand()
187
+ }
188
+ function move(event) {
189
+ updateResize(event)
190
+ updateTextHit(event)
191
+ if (handCursor) followHand(event.target?.nodeType === 1 ? event.target : event.target?.parentElement)
192
+ if (pointerId !== undefined && event.pointerId === pointerId && !(event.buttons & 1)) reset()
193
+ }
194
+ function resetInteraction() {
195
+ reset()
196
+ clearResize()
197
+ clearTextHit()
198
+ lastPoint = undefined
199
+ }
200
+ function leave(event) { if (!event.relatedTarget) resetInteraction() }
201
+ function blur(event) {
202
+ // Captured element blur also reaches window when focus changes inside the app.
203
+ if (event.target === window) resetInteraction()
204
+ }
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]]
207
+ function dispose() {
208
+ resetInteraction()
209
+ if (!listening) return
210
+ for (const [name, handler] of events) window.removeEventListener(name, handler, true)
211
+ document.removeEventListener('visibilitychange', visibility)
212
+ listening = false
213
+ }
214
+ return {
215
+ update(preferences) {
216
+ if (!root || !assets || !preferences.enabled || preferences.cursor !== 'prts') { dispose(); return }
217
+ if (listening) return
218
+ for (const [name, handler] of events) window.addEventListener(name, handler, true)
219
+ document.addEventListener('visibilitychange', visibility)
220
+ listening = true
221
+ },
222
+ dispose,
223
+ }
224
+ }
@@ -79,6 +79,7 @@ export function applyPrtsPlugin(ctx, environment) {
79
79
  window,
80
80
  cssText,
81
81
  service: themeService,
82
+ cursorAssets: assets.cursor,
82
83
  onTransitionStateChange(active) {
83
84
  particleField.setSchemeTransitionActive(active)
84
85
  },
@@ -24,6 +24,7 @@ export const DEFAULT_PREFERENCES = Object.freeze({
24
24
  glass: 'standard',
25
25
  motion: 'system',
26
26
  bootAnimation: true,
27
+ cursor: 'prts',
27
28
  conversationParticleDensity: 'sparse',
28
29
  heroParticleDensity: 'light',
29
30
  particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
@@ -76,7 +77,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
76
77
  background: Object.freeze(['texture']),
77
78
  particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
78
79
  material: Object.freeze(['glass']),
79
- accessibility: Object.freeze(['motion', 'bootAnimation']),
80
+ accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
80
81
  navigation: Object.freeze(['railDefaultHidden']),
81
82
  conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle']),
82
83
  })
@@ -178,6 +179,7 @@ export function normalizePreferences(value) {
178
179
  glass: normalizeGlass(input),
179
180
  motion: input.motion === 'full' ? 'system' : enumOr(input.motion, MOTIONS, DEFAULT_PREFERENCES.motion),
180
181
  bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
182
+ cursor: input.cursor === 'system' ? 'system' : DEFAULT_PREFERENCES.cursor,
181
183
  conversationParticleDensity: enumOr(legacyDensities.conversation, PARTICLE_DENSITIES, particlePreset.conversationParticleDensity),
182
184
  heroParticleDensity: enumOr(legacyDensities.hero, PARTICLE_DENSITIES, particlePreset.heroParticleDensity),
183
185
  particleTraversalSpeed: particleTraversalSpeedOr(
@@ -1,3 +1,4 @@
1
+ import { createCursorAdapter } from './cursor-adapter.js'
1
2
  import { normalizePreferences } from './preferences.js'
2
3
 
3
4
  const ROOT_ATTRIBUTES = [
@@ -6,6 +7,7 @@ const ROOT_ATTRIBUTES = [
6
7
  'data-prts-texture',
7
8
  'data-prts-glass',
8
9
  'data-prts-motion',
10
+ 'data-prts-cursor',
9
11
  'data-prts-particle-pattern',
10
12
  'data-prts-session-flow',
11
13
  'data-prts-session-glow',
@@ -67,12 +69,13 @@ function resolveScheme(value) {
67
69
  return identity.includes('dark') || identity.includes('night') ? 'dark' : 'light'
68
70
  }
69
71
 
70
- export function createThemeController({ document, window, cssText, service, onTransitionStateChange = () => {} }) {
72
+ export function createThemeController({ document, window, cssText, service, cursorAssets, onTransitionStateChange = () => {} }) {
71
73
  if (!document || !window) {
72
74
  const gesture = { update() {}, finish() {}, cancel() {} }
73
75
  return { apply() {}, sync() {}, refresh() {}, setTheme() {}, beginThemeTransition() { return gesture }, toggle() {}, dispose() {} }
74
76
  }
75
77
 
78
+ const cursor = createCursorAdapter({ document, window, assets: cursorAssets })
76
79
  const root = document.documentElement
77
80
  let style
78
81
  let disposed = false
@@ -115,6 +118,7 @@ export function createThemeController({ document, window, cssText, service, onTr
115
118
  }
116
119
 
117
120
  function removeOwnedState() {
121
+ cursor.dispose()
118
122
  disposed = true
119
123
  refreshRevision += 1
120
124
  setRevision += 1
@@ -712,6 +716,7 @@ export function createThemeController({ document, window, cssText, service, onTr
712
716
  disposed = false
713
717
  const preferences = normalizePreferences(value)
714
718
  ensureStyle()
719
+ cursor.update(preferences)
715
720
  if (!preferences.enabled) {
716
721
  cancelActiveTransition()
717
722
  clearThemeState()
@@ -721,6 +726,7 @@ export function createThemeController({ document, window, cssText, service, onTr
721
726
  root.dataset.prtsTexture = preferences.texture
722
727
  root.dataset.prtsGlass = preferences.glass
723
728
  root.dataset.prtsMotion = preferences.motion
729
+ root.dataset.prtsCursor = preferences.cursor
724
730
  root.dataset.prtsParticlePattern = 'orthogonal'
725
731
  root.dataset.prtsSessionFlow = preferences.sessionFlow ? 'on' : 'off'
726
732
  root.dataset.prtsSessionGlow = preferences.sessionGlow ? 'on' : 'off'
@@ -15,6 +15,7 @@ const PRESET_OPTIONS = [
15
15
  ]
16
16
 
17
17
  const COMMON_CONTROLS = [
18
+ ['cursor', 'PRTS 光标', [['system', '关闭'], ['prts', '开启']]],
18
19
  ['railDefaultHidden', '默认隐藏导航栏', [['false', '关闭'], ['true', '开启']]],
19
20
  ['texture', '环境底纹', [['off', '关闭'], ['restrained', '克制'], ['full', '完整']]],
20
21
  ['glass', '玻璃材质', [['off', '关闭'], ['soft', '柔和'], ['standard', '标准'], ['clear', '清晰']]],
@@ -52,6 +53,10 @@ function settingButtons(key, label, options) {
52
53
  : key === 'sessionFlowPalette'
53
54
  ? '<small data-prts-setting-note>同时适配日间与夜间模式</small>'
54
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>`
55
60
  const sample = value => key === 'texture' || key === 'glass' || key === 'sessionFlowPalette'
56
61
  ? `<i data-prts-option-sample="${key}" data-prts-option-sample-value="${value}" aria-hidden="true"></i>`
57
62
  : ''
@@ -113,26 +118,47 @@ export function themeSettingsMarkup() {
113
118
  <button type="button" data-prts-theme-settings-close aria-label="关闭主题设置">×</button>
114
119
  </header>
115
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>
116
128
  <div data-prts-theme-settings-scroll>
117
- <section data-prts-settings-presets>
118
- <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>
119
131
  <div>${presetMarkup()}</div>
120
132
  </section>
121
133
 
122
134
  <section data-prts-settings-common>
123
- <header><span><small>常用设置</small><strong>主题表现</strong></span></header>
124
- <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('')}
125
151
  ${sessionFlowColorMarkup()}
126
152
  <label data-prts-setting-row="sessionFlowSpeed" data-prts-setting-range-row data-prts-session-flow-speed>
127
153
  <span><strong>流线速度</strong><small data-prts-setting-note>控制当前会话流线的形变速度</small></span>
128
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>
129
155
  </label>
130
- ${settingButtons('conversationStyle', '会话主题', [['native', '原有'], ['deck-chat', '通讯链路']])}
156
+
131
157
  </div>
132
158
  </section>
133
159
 
134
- <details data-prts-settings-advanced>
135
- <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>
136
162
  <div data-prts-settings-advanced-content>
137
163
  <section data-prts-particle-detail>
138
164
  <header><strong>粒子精度</strong><span>同时调整会话与新会话徽记</span></header>
@@ -162,6 +188,7 @@ export function themeSettingsMarkup() {
162
188
  </div>
163
189
  </footer>
164
190
  </div>
191
+ </div>
165
192
  </div>
166
193
  </dialog>`
167
194
  }
@@ -428,6 +455,18 @@ export function createThemeSettingsOverlay({
428
455
 
429
456
  function renderState() {
430
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
+ }
431
470
  const particleDetail = resolveParticleDetail(currentPreferences)
432
471
  for (const button of panel.querySelectorAll('button[data-prts-setting-key]')) {
433
472
  const key = button.dataset.prtsSettingKey
@@ -481,7 +520,23 @@ export function createThemeSettingsOverlay({
481
520
  if (event.target === panel) { setOpen(false); return }
482
521
  const target = event.target?.closest?.('button')
483
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
+ }
484
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
+ }
485
540
  if (target.hasAttribute('data-prts-setting-key')) {
486
541
  const raw = target.dataset.prtsSettingValue
487
542
  if (target.dataset.prtsSettingKey === 'sessionFlowPalette') {
@@ -0,0 +1,21 @@
1
+ /* Native cursor; gesture frames override the resting image without a DOM overlay. */
2
+ @media (any-pointer: fine) {
3
+ html[data-dsh-prts][data-prts-cursor="prts"] {
4
+ --prts-hand-cursor: var(--prts-hand-frame, url("__PRTS_HAND__") 7 6);
5
+ --prts-text-cursor: var(--prts-text-frame, url("__PRTS_TEXT__") 24 24);
6
+ --prts-link-cursor: url("__PRTS_LINK__") 7 7;
7
+ cursor: var(--prts-hand-cursor), default;
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"]) {
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;
14
+ }
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"]) {
16
+ cursor: var(--prts-text-cursor), text;
17
+ }
18
+ html[data-dsh-prts][data-prts-cursor="prts"] :is(:disabled, [aria-disabled="true"]) {
19
+ cursor: not-allowed !important;
20
+ }
21
+ }