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

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.4",
3
+ "version": "2.6.0",
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",
@@ -69,7 +69,7 @@
69
69
  },
70
70
  "engines": {
71
71
  "node": ">=20.9.0",
72
- "dsh": "0.2.1-rc.2 || 0.2.1-rc.3"
72
+ "dsh": "0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.5"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@playwright/test": "1.55.1",
@@ -23,6 +23,8 @@ function isPendingInteractionAction(button) {
23
23
  function classify(button, operation) {
24
24
  // The host's process disclosure owns its transparent, full-width hit area.
25
25
  if (button.hasAttribute('data-turn-process')) return null
26
+ // Schedule cards use an absolute transparent button as the whole-card hit area.
27
+ if (button.parentElement?.matches('[data-tool="schedule_create"]') && hasClassSuffix(button, '_cardOpen')) return null
26
28
  if (button.querySelector('[data-compaction-disclosure]')) return null
27
29
  // Notification headers belong to the card surface, not standalone buttons.
28
30
  if (button.parentElement?.matches('[data-turn-trigger]')) return null
@@ -183,10 +185,22 @@ function mountBodyTargetFrame(document, window, operation) {
183
185
  const find=node=>{
184
186
  if(activeModal && !activeModal.contains(node))return null
185
187
  if (!node?.closest || node.closest('[data-prts-workspace-actions],[data-prts-session-actions]')) return null
186
- const card=node.closest('[data-presented-file],[data-changed-files]')
188
+ const container=node.closest('[data-presented-file],[data-changed-files],[data-tool="schedule_create"]')
187
189
  const button=node.closest('button')
190
+ let cardTarget=null
191
+ if(container){
192
+ const action=node.closest('button,a[href],[role="button"],[role="link"],summary,input:not([type="hidden"]),select,textarea')
193
+ const nested=action && action!==container && container.contains(action)
194
+ // Disabled controls must not fall back to their enclosing card.
195
+ if(node.closest(':disabled,[aria-disabled="true"],[inert]'))return null
196
+ const hitLayer=nested && action.tagName==='BUTTON' && (
197
+ container.matches('[data-presented-file]') && hasClassSuffix(action,'_cardPreview') ||
198
+ action.parentElement===container && container.matches('[data-tool="schedule_create"]') && hasClassSuffix(action,'_cardOpen'))
199
+ const openable=!container.matches('[data-tool="schedule_create"]') || [...container.children].some(child=>child.tagName==='BUTTON' && hasClassSuffix(child,'_cardOpen') && !child.matches(':disabled,[aria-disabled="true"]'))
200
+ cardTarget=nested && !hitLayer ? action : openable ? container : null
201
+ }
188
202
  const pendingAction=operation.contains(button) && isPendingInteractionAction(button) ? button : null
189
- const candidate=card || pendingAction || node.closest(selector)
203
+ const candidate=cardTarget || pendingAction || node.closest(selector)
190
204
  return candidate && !candidate.matches(':disabled,[aria-disabled="true"]') && !candidate.closest('[inert]') ? candidate : null
191
205
  }
192
206
  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}
@@ -7,6 +7,7 @@ export function createCursorAdapter({ document, window, assets }) {
7
7
  let handPhase
8
8
  let gestureAssets = assets
9
9
  let textTarget
10
+ let moveFrame, pendingMove
10
11
  let lastPoint
11
12
  const heroTextSelector = '[data-phase="hero"] [class$="_titleGroup"]'
12
13
  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"]'
@@ -90,6 +91,7 @@ export function createCursorAdapter({ document, window, assets }) {
90
91
  let originalPriority
91
92
  let appliedCursor
92
93
  let appliedPriority
94
+ let lastHandFrame
93
95
  function ownsHandCursor() {
94
96
  return handTarget?.style.cursor === appliedCursor
95
97
  && handTarget.style.getPropertyPriority('cursor') === appliedPriority
@@ -106,6 +108,7 @@ export function createCursorAdapter({ document, window, assets }) {
106
108
  }
107
109
  handTarget?.removeAttribute('data-prts-hand-active')
108
110
  handTarget = undefined
111
+ lastHandFrame = undefined
109
112
  }
110
113
  // Cursor is inherited. Animating a large container still restyles its subtree.
111
114
  // Keep those background hits static, while controls retain native animation.
@@ -130,7 +133,9 @@ export function createCursorAdapter({ document, window, assets }) {
130
133
  }
131
134
  function applyHandFrame() {
132
135
  if (stopIfHandChanged() || !handTarget) return
136
+ if(lastHandFrame === handCursor && ownsHandCursor())return
133
137
  handTarget.style.setProperty('cursor', handCursor)
138
+ lastHandFrame = handCursor
134
139
  appliedCursor = handTarget.style.cursor
135
140
  appliedPriority = handTarget.style.getPropertyPriority('cursor')
136
141
  }
@@ -156,20 +161,27 @@ export function createCursorAdapter({ document, window, assets }) {
156
161
  }
157
162
  function play(frames, done) {
158
163
  clearTimer()
159
- let index = 0
164
+ const started = window.performance.now()
165
+ let index = 0, deadline = frames[0]?.duration ?? 0
160
166
  function next() {
161
167
  timer = undefined
162
168
  if (stopIfHandChanged()) return
163
- // The source ANI ends with a 60-second hold frame. Use the static state instead.
169
+ const elapsed = window.performance.now() - started
170
+ // Skip expired frames after a busy main thread; never replay a backlog.
171
+ while (index < frames.length - 1 && elapsed >= deadline) {
172
+ index += 1
173
+ deadline += frames[index]?.duration ?? 0
174
+ }
175
+ // The source ANI ends with a 60-second hold frame, which is not animated.
164
176
  if (index >= frames.length - 1) { done(); return }
165
- const frame = frames[index++]
166
- show(frame.url)
177
+ show(frames[index].url)
167
178
  if (!handCursor) return
168
- timer = window.setTimeout(next, frame.duration)
179
+ timer = window.setTimeout(next, Math.max(1, deadline - (window.performance.now() - started)))
169
180
  }
170
181
  next()
171
182
  }
172
183
  function down(event) {
184
+ clearPendingMove()
173
185
  if (event.pointerType !== 'mouse' || event.button !== 0 || event.isPrimary === false) return
174
186
  updateResize(event)
175
187
  updateTextHit(event)
@@ -203,13 +215,32 @@ export function createCursorAdapter({ document, window, assets }) {
203
215
  if (handPhase === 'press') return
204
216
  releaseHand()
205
217
  }
206
- function move(event) {
218
+ function applyMove(event) {
207
219
  updateResize(event)
208
220
  updateTextHit(event)
209
221
  if (handCursor) followHand(event.target?.nodeType === 1 ? event.target : event.target?.parentElement)
210
222
  if (pointerId !== undefined && event.pointerId === pointerId && !(event.buttons & 1)) reset()
211
223
  }
224
+ function clearPendingMove() {
225
+ if (moveFrame !== undefined) window.cancelAnimationFrame?.(moveFrame)
226
+ moveFrame = undefined
227
+ pendingMove = undefined
228
+ }
229
+ function move(event) {
230
+ if (event.pointerType !== 'mouse') return
231
+ // Lost releases must clear synchronously even when the next paint is delayed.
232
+ if (pointerId !== undefined && event.pointerId === pointerId && !(event.buttons & 1)) reset()
233
+ if (!window.requestAnimationFrame) { applyMove(event); return }
234
+ pendingMove = { target:event.target, pointerType:event.pointerType, pointerId:event.pointerId,
235
+ buttons:event.buttons, clientX:event.clientX, clientY:event.clientY }
236
+ if (moveFrame === undefined) moveFrame = window.requestAnimationFrame(() => {
237
+ const latest = pendingMove
238
+ clearPendingMove()
239
+ if (latest?.target?.isConnected) applyMove(latest)
240
+ })
241
+ }
212
242
  function resetInteraction() {
243
+ clearPendingMove()
213
244
  reset()
214
245
  clearResize()
215
246
  clearTextHit()
@@ -223,7 +223,11 @@ 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
+ if (key === 'cornerColor' || key === 'cornerColorLight') {
227
+ const properties = preferences.cornerColorLinked ? ['--prts-corner-color-light', '--prts-corner-color-dark'] : [key === 'cornerColorLight' ? '--prts-corner-color-light' : '--prts-corner-color-dark']
228
+ for (const property of properties) document.documentElement.style.setProperty(property, String(value))
229
+ return
230
+ }
227
231
  if (key === 'particleTraversalSpeed') {
228
232
  particleField.update(updatePreferenceValue(preferences, key, value))
229
233
  return
@@ -28,11 +28,15 @@ export function createNativePreferences({ storage, onChange }) {
28
28
  migrationAttempted = true
29
29
  pending = { ...legacy, ...pending }
30
30
  }
31
- // Materialize the old shared sidebar values once, so later left-side edits
32
- // cannot change a right-side preference that was never written by the Host.
31
+ // Materialize legacy shared values before either side can be edited independently.
33
32
  if (s.value?.preferencesInitialized && writable(s)) {
34
- for (const key of ['rightSidebarOpacity', 'rightSidebarBlur']) {
35
- if (!Object.hasOwn(s.value, key) && !Object.hasOwn(pending, key) && !Object.hasOwn(inFlight, key) && !Object.hasOwn(failed, key)) pending[key] = host[key]
33
+ if (s.value.cornerColorSource == null && s.value.cornerColorLinked === true) {
34
+ for (const key of ['cornerColor', 'cornerCustomColor']) {
35
+ if (s.value[key] !== host[key] && !Object.hasOwn(pending, key) && !Object.hasOwn(inFlight, key) && !Object.hasOwn(failed, key)) pending[key] = host[key]
36
+ }
37
+ }
38
+ for (const key of ['rightSidebarOpacity', 'rightSidebarBlur', 'cornerColorLight', 'cornerCustomColorLight', 'cornerColorSource']) {
39
+ if (s.value[key] == null && !Object.hasOwn(pending, key) && !Object.hasOwn(inFlight, key) && !Object.hasOwn(failed, key)) pending[key] = host[key]
36
40
  }
37
41
  }
38
42
  current = normalizePreferences({ ...host, ...inFlight, ...pending })
@@ -39,8 +39,12 @@ export const DEFAULT_PREFERENCES = Object.freeze({
39
39
  bakerBlur: 8,
40
40
  classicUserBlur: 8,
41
41
  cursor: 'prts',
42
+ cornerColorLinked: false,
43
+ cornerColorSource: 'dark',
42
44
  cornerColor: '#f0c800',
43
45
  cornerCustomColor: '#f0c800',
46
+ cornerColorLight: '#f0c800',
47
+ cornerCustomColorLight: '#f0c800',
44
48
  conversationParticleDensity: 'sparse',
45
49
  heroParticleDensity: 'light',
46
50
  particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
@@ -107,7 +111,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
107
111
  background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'heroContrast', 'sidebarOpacity', 'rightSidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'rightSidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
108
112
  particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
109
113
  material: Object.freeze(['glass', 'composerGlass']),
110
- accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor', 'cornerColor', 'cornerCustomColor']),
114
+ accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor', 'cornerColorLinked', 'cornerColorSource', 'cornerColor', 'cornerCustomColor', 'cornerColorLight', 'cornerCustomColorLight']),
111
115
  aiTools: Object.freeze(['aiToolsEnabled']),
112
116
  navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewTools', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
113
117
  conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'bakerLoading', 'conversationEmblem']),
@@ -231,8 +235,12 @@ export function normalizePreferences(value) {
231
235
  backgroundDim: typeof input.backgroundDim === 'number' && Number.isFinite(input.backgroundDim) ? Math.min(.9, Math.max(0, input.backgroundDim)) : .45,
232
236
  composerGlass: booleanOr(input.composerGlass, DEFAULT_PREFERENCES.composerGlass),
233
237
  bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
238
+ cornerColorLinked: booleanOr(input.cornerColorLinked, false),
239
+ cornerColorSource: input.cornerColorSource === 'light' ? 'light' : 'dark',
234
240
  cornerCustomColor: colorOr(input.cornerCustomColor, DEFAULT_PREFERENCES.cornerCustomColor),
235
241
  cornerColor: colorOr(input.cornerColor, DEFAULT_PREFERENCES.cornerColor),
242
+ cornerColorLight: colorOr(input.cornerColorLight, colorOr(input.cornerColor, DEFAULT_PREFERENCES.cornerColor)),
243
+ cornerCustomColorLight: colorOr(input.cornerCustomColorLight, colorOr(input.cornerCustomColor, DEFAULT_PREFERENCES.cornerCustomColor)),
236
244
  cursor: input.cursor === 'system' ? 'system' : DEFAULT_PREFERENCES.cursor,
237
245
  conversationParticleDensity: enumOr(legacyDensities.conversation, PARTICLE_DENSITIES, particlePreset.conversationParticleDensity),
238
246
  heroParticleDensity: enumOr(legacyDensities.hero, PARTICLE_DENSITIES, particlePreset.heroParticleDensity),
@@ -263,6 +271,11 @@ export function normalizePreferences(value) {
263
271
  bakerGroupAvatars: booleanOr(input.bakerGroupAvatars, DEFAULT_PREFERENCES.bakerGroupAvatars),
264
272
  conversationStyle: enumOr(input.conversationStyle, CONVERSATION_STYLES, DEFAULT_PREFERENCES.conversationStyle),
265
273
  }
274
+ // Preserve the active color from the interim shared-palette format on upgrade.
275
+ if (input.cornerColorSource == null && input.cornerColorLinked === true) {
276
+ normalized.cornerColor = colorOr(input.cornerColorShared, normalized.cornerColor)
277
+ normalized.cornerCustomColor = colorOr(input.cornerCustomColorShared, normalized.cornerCustomColor)
278
+ }
266
279
  normalized.preset = matchedVisualPreset(normalized)
267
280
  return normalized
268
281
  }
@@ -287,9 +300,16 @@ export function updatePreferenceValue(value, key, nextValue) {
287
300
  if (key === 'preset') return applyVisualPreset(value, nextValue)
288
301
  if (key === 'particleDetail') return applyParticleDetail(value, nextValue)
289
302
  const current = normalizePreferences(value)
290
- if (key === 'cornerCustomColor') {
291
- const color = colorOr(nextValue, current.cornerCustomColor)
292
- return normalizePreferences({ ...current, cornerColor: color, cornerCustomColor: color })
303
+ if (key === 'cornerColorLink') {
304
+ const enabled = nextValue === 'light' || nextValue === 'dark'
305
+ return normalizePreferences({ ...current, cornerColorLinked: enabled,
306
+ ...(enabled ? { cornerColorSource: nextValue } : {}) })
307
+ }
308
+ if (['cornerColor', 'cornerColorLight', 'cornerCustomColor', 'cornerCustomColorLight'].includes(key)) {
309
+ const color = colorOr(nextValue, current[key])
310
+ const custom = key.startsWith('cornerCustom')
311
+ const colorKey = custom ? key.replace('cornerCustomColor', 'cornerColor') : key
312
+ return normalizePreferences({ ...current, [colorKey]: color, ...(custom ? { [key]: color } : {}) })
293
313
  }
294
314
  if (key === 'sessionFlowPalette') {
295
315
  const palette = enumOr(nextValue, SESSION_FLOW_PALETTE_IDS, current.sessionFlowPalette)
@@ -27,9 +27,12 @@ export function createPreviewContentCache(document, render = renderPreviewMarkdo
27
27
  const html = render(text)
28
28
  const plain = document.createElement('div')
29
29
  plain.innerHTML = html
30
- const summary = plain.textContent.replace(/\s+/g, ' ').trim().slice(0, 100)
30
+ const fullSummary = plain.textContent.replace(/\s+/g, ' ').trim()
31
+ const summary = fullSummary.slice(0, 100)
32
+ const truncated = fullSummary.length > summary.length
33
+ const needsDetail = truncated || Boolean(plain.querySelector('a,pre,ul,ol,table,blockquote,h1,h2,h3,h4,h5,h6,br') || plain.children.length > 1)
31
34
  const cost = text.length + html.length + summary.length
32
- const value = {html, summary, cost}
35
+ const value = {html, summary, cost, needsDetail, truncated}
33
36
  if (cost <= 262144) {
34
37
  while (entries.size && (entries.size >= 32 || size + cost > 262144)) {
35
38
  const key = entries.keys().next().value
@@ -293,12 +293,29 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
293
293
  const height = Math.max(1, (viewport?.height ?? window.innerHeight) - 16)
294
294
  const place = (surface, reference, preferredTop, noticeOwner, anchorBottom = preferredTop) => {
295
295
  surface.style.maxWidth = 'none'
296
- surface.style.maxHeight = 'none'
296
+ let fixedEdge
297
+ if (surface === panel) {
298
+ // Pick a direction once, before async replies arrive; keep the same edge
299
+ // while content grows and let the existing scroll surface absorb overflow.
300
+ if (!current.placement) {
301
+ const below = top + height - preferredTop
302
+ const above = anchorBottom - top
303
+ current.placement = below < 160 && above > below
304
+ ? { direction: 'above', edge: anchorBottom }
305
+ : { direction: 'below', edge: preferredTop }
306
+ }
307
+ fixedEdge = Math.max(top, Math.min(current.placement.edge, top + height))
308
+ const available = current.placement.direction === 'above' ? fixedEdge - top : top + height - fixedEdge
309
+ surface.style.maxHeight = Math.max(1, available) + 'px'
310
+ surface.setAttribute('data-prts-preview-direction', current.placement.direction)
311
+ } else surface.style.maxHeight = 'none'
297
312
  const bounds = surface.getBoundingClientRect()
298
313
  const x = reference ? reference.right + 10 : left
299
- const fitsVertically = y => y >= top && y + bounds.height <= top + height
314
+ const fitsVertically = y => y >= top - 1 && y + bounds.height <= top + height + 1
300
315
  const above = anchorBottom - bounds.height
301
- const y = !fitsVertically(preferredTop) && fitsVertically(above) ? above : preferredTop
316
+ const y = surface === panel
317
+ ? current.placement.direction === 'above' ? fixedEdge - bounds.height : fixedEdge
318
+ : !fitsVertically(preferredTop) && fitsVertically(above) ? above : preferredTop
302
319
  const insufficient = Boolean(reference) && (x < left || x + bounds.width > left + width || !fitsVertically(y))
303
320
  surface.toggleAttribute('data-prts-preview-no-space', insufficient)
304
321
  const noticeKey = surface === panel ? 'spaceNotice' : 'detailSpaceNotice'
@@ -319,6 +336,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
319
336
  surface.style.top = y + 'px'
320
337
  return !insufficient
321
338
  }
339
+ updateReplyAvailability()
322
340
  const menu = current.anchor?.closest('[data-prts-session-summary-popover]')
323
341
  const reference = menu?.getBoundingClientRect()
324
342
  const row = current.anchor?.getBoundingClientRect()
@@ -343,6 +361,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
343
361
  current.spaceNotice?.remove()
344
362
  current.detailSpaceNotice?.remove()
345
363
  panel.removeAttribute('data-prts-preview-no-space')
364
+ panel.removeAttribute('data-prts-preview-direction')
365
+ panel.style.removeProperty('max-height')
346
366
  positionObserver?.disconnect()
347
367
  window.cancelAnimationFrame(positionFrame)
348
368
  window.removeEventListener('resize', schedulePosition)
@@ -537,8 +557,24 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
537
557
  detailEntry?.removeAttribute('data-prts-reply-active')
538
558
  detailEntry = undefined
539
559
  }
560
+ const replyIsTruncated = item => {
561
+ const summary = item.querySelector('span')
562
+ return item.dataset.prtsSummaryTruncated === 'true' || summary.scrollHeight > summary.clientHeight + 1 || summary.scrollWidth > summary.clientWidth + 1
563
+ }
564
+ const updateReplyAvailability = () => {
565
+ const entries = [...body.querySelectorAll('[data-prts-reply-entry]')].map(item => [item, replyIsTruncated(item)])
566
+ for (const [item, truncated] of entries) {
567
+ const needed = truncated || item.dataset.prtsReplyTruncated === 'true'
568
+ item.toggleAttribute('data-prts-reply-overflow', truncated)
569
+ item.toggleAttribute('data-prts-reply-complete', !needed)
570
+ item.tabIndex = needed ? 0 : -1
571
+ item.setAttribute('aria-disabled', String(!needed))
572
+ if (!needed && detailEntry === item) hideDetail()
573
+ }
574
+ }
540
575
  const showDetail = (reply, index, item, refresh = false) => {
541
576
  keep()
577
+ if (!reply.tool && item.hasAttribute('data-prts-reply-complete')) { hideDetail(); return }
542
578
  if (!refresh && detailKey === reply.key && current.detail && !current.detail.hidden) return
543
579
  if (!current.detail) {
544
580
  const detail = panel.ownerDocument.createElement('section')
@@ -708,8 +744,15 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
708
744
  icon.append(outline)
709
745
  heading.prepend(icon)
710
746
  const summary = panel.ownerDocument.createElement('span')
711
- summary.textContent = current.contentCache(reply.text).summary
712
- item.append(heading, summary)
747
+ const content = current.contentCache(reply.text)
748
+ summary.textContent = content.summary
749
+ item.dataset.prtsReplyTruncated = String(content.needsDetail)
750
+ item.dataset.prtsSummaryTruncated = String(content.truncated)
751
+ const ellipsis = panel.ownerDocument.createElement('i')
752
+ ellipsis.setAttribute('data-prts-reply-ellipsis', '')
753
+ ellipsis.setAttribute('aria-hidden', 'true')
754
+ ellipsis.textContent = '…'
755
+ item.append(heading, summary, ellipsis)
713
756
  item.addEventListener('pointermove', () => showDetail(reply, index, item))
714
757
  item.addEventListener('focus', () => showDetail(reply, index, item))
715
758
  item.addEventListener('click', () => showDetail(reply, index, item))
@@ -725,6 +768,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
725
768
  if (scrollAnchor?.isConnected) body.scrollTop += scrollAnchor.getBoundingClientRect().top - anchorTop
726
769
  else if (previousTop === 0) body.scrollTop = 0
727
770
  historyKeys = replies
771
+ schedulePosition()
728
772
  previousText = undefined
729
773
  previousQuestion = undefined
730
774
  return
@@ -19,7 +19,8 @@ const ROOT_ATTRIBUTES = [
19
19
  'data-prts-background-mode',
20
20
  ]
21
21
  const SESSION_FLOW_COLOR_PROPERTIES = {
22
- '--prts-corner-color': 'cornerColor',
22
+ '--prts-corner-color-dark': 'cornerColor',
23
+ '--prts-corner-color-light': 'cornerColorLight',
23
24
  '--prts-session-flow-back': 'sessionFlowColorBack',
24
25
  '--prts-session-flow-core': 'sessionFlowColorCore',
25
26
  '--prts-session-flow-front': 'sessionFlowColorFront',
@@ -766,7 +767,7 @@ export function createThemeController({ document, window, cssText, service, curs
766
767
  root.dataset.prtsSessionGlow = preferences.sessionGlow ? 'on' : 'off'
767
768
  root.dataset.prtsSessionFlowPalette = preferences.sessionFlowPalette
768
769
  root.dataset.prtsSessionFlowSpeed = String(preferences.sessionFlowSpeed)
769
- for (const [property, key] of Object.entries(SESSION_FLOW_COLOR_PROPERTIES)) root.style.setProperty(property, preferences[key])
770
+ for (const [property, key] of Object.entries(SESSION_FLOW_COLOR_PROPERTIES)) root.style.setProperty(property, preferences.cornerColorLinked && (key === 'cornerColor' || key === 'cornerColorLight') ? preferences[preferences.cornerColorSource === 'light' ? 'cornerColorLight' : 'cornerColor'] : preferences[key])
770
771
  root.dataset.prtsConversationStyle = preferences.conversationStyle
771
772
  root.toggleAttribute('data-prts-baker-loading', preferences.bakerLoading === true)
772
773
  root.dataset.prtsBackgroundMode = preferences.backgroundMode
@@ -14,7 +14,8 @@ const ROOT_ATTRIBUTES = [
14
14
  'data-prts-conversation-style',
15
15
  ]
16
16
  const SESSION_FLOW_COLOR_PROPERTIES = {
17
- '--prts-corner-color': 'cornerColor',
17
+ '--prts-corner-color-dark': 'cornerColor',
18
+ '--prts-corner-color-light': 'cornerColorLight',
18
19
  '--prts-session-flow-back': 'sessionFlowColorBack',
19
20
  '--prts-session-flow-core': 'sessionFlowColorCore',
20
21
  '--prts-session-flow-front': 'sessionFlowColorFront',
@@ -124,7 +125,7 @@ export function createThemeController({ document, window, cssText, service }) {
124
125
  root.dataset.prtsSessionGlow = preferences.sessionGlow ? 'on' : 'off'
125
126
  root.dataset.prtsSessionFlowPalette = preferences.sessionFlowPalette
126
127
  root.dataset.prtsSessionFlowSpeed = String(preferences.sessionFlowSpeed)
127
- for (const [property, key] of Object.entries(SESSION_FLOW_COLOR_PROPERTIES)) root.style.setProperty(property, preferences[key])
128
+ for (const [property, key] of Object.entries(SESSION_FLOW_COLOR_PROPERTIES)) root.style.setProperty(property, preferences.cornerColorLinked && (key === 'cornerColor' || key === 'cornerColorLight') ? preferences[preferences.cornerColorSource === 'light' ? 'cornerColorLight' : 'cornerColor'] : preferences[key])
128
129
  root.dataset.prtsConversationStyle = preferences.conversationStyle
129
130
  refresh()
130
131
  },
@@ -171,7 +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
+ <div data-prts-setting-row="cornerColor"><span><span data-prts-corner-heading><strong>交互提示框颜色</strong><span data-prts-corner-modes role="group" aria-label="编辑配色模式"><button type="button" data-prts-corner-mode="light" aria-pressed="false">☀ 日间</button><button type="button" data-prts-corner-mode="dark" aria-pressed="false">☾ 夜间</button></span><button type="button" data-prts-corner-sync role="switch" aria-checked="false" title="开启后,日间和夜间共用颜色"><span>日夜共用</span><i aria-hidden="true"></i></button></span><small data-prts-setting-note>部分按钮和卡片的四角提示颜色,危险操作保持红色。</small><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('')}<span data-prts-corner-picker><button type="button" data-prts-corner-custom aria-expanded="false" title="选择自定义颜色"><i data-prts-corner-preview aria-hidden="true"></i><span>自定义</span></button><input type="color" data-prts-corner-color aria-label="交互提示框颜色" value="#f0c800" tabindex="-1"><span data-prts-color-editor hidden role="group" aria-label="自定义提示框颜色"><span data-prts-color-editor-title>自定义颜色</span><span data-prts-color-sample></span><label data-prts-color-hex-label>HEX<input data-prts-color-hex aria-label="HEX 颜色" maxlength="7" spellcheck="false"></label><label data-prts-color-channel><span>R</span><input type="range" min="0" max="255" step="1" data-prts-color-channel-index="0" aria-label="R 通道"><output>0</output></label><label data-prts-color-channel><span>G</span><input type="range" min="0" max="255" step="1" data-prts-color-channel-index="1" aria-label="G 通道"><output>0</output></label><label data-prts-color-channel><span>B</span><input type="range" min="0" max="255" step="1" data-prts-color-channel-index="2" aria-label="B 通道"><output>0</output></label><span data-prts-color-actions><button type="button" data-prts-color-cancel>取消</button><button type="button" data-prts-color-apply>应用颜色</button></span></span></span></div></div>
175
175
  </div>
176
176
  </section>
177
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>
@@ -285,6 +285,35 @@ export function createThemeSettingsOverlay({
285
285
  const nativeModal = typeof panel?.showModal === 'function' && typeof panel?.close === 'function'
286
286
  let isOpen = false
287
287
  let previousFocus
288
+ let colorDraft = '#f0c800'
289
+ function closeColorEditor(restoreFocus = false) {
290
+ const editor = panel?.querySelector('[data-prts-color-editor]')
291
+ if (!editor || editor.hidden) return
292
+ editor.hidden = true
293
+ panel.querySelector('[data-prts-corner-custom]').setAttribute('aria-expanded', 'false')
294
+ onPreferencePreview(cornerKey(), currentPreferences[cornerKey()])
295
+ if (restoreFocus) panel.querySelector('[data-prts-corner-custom]').focus()
296
+ }
297
+ function renderColorDraft() {
298
+ const editor = panel.querySelector('[data-prts-color-editor]')
299
+ editor.querySelector('[data-prts-color-sample]').style.background = colorDraft
300
+ const hex = editor.querySelector('[data-prts-color-hex]')
301
+ hex.value = colorDraft.toUpperCase()
302
+ hex.removeAttribute('aria-invalid')
303
+ const channels = [1, 3, 5].map(start => parseInt(colorDraft.slice(start, start + 2), 16))
304
+ for (const slider of editor.querySelectorAll('[data-prts-color-channel-index]')) {
305
+ const index = Number(slider.dataset.prtsColorChannelIndex)
306
+ slider.value = channels[index]
307
+ slider.nextElementSibling.value = channels[index]
308
+ const end = [...channels], start = [...channels]
309
+ start[index] = 0; end[index] = 255
310
+ slider.style.background = 'linear-gradient(to right,rgb(' + start.join(',') + '),rgb(' + end.join(',') + '))'
311
+ }
312
+ }
313
+ let cornerEditMode = 'dark'
314
+ const cornerMode = () => currentPreferences?.cornerColorLinked ? currentPreferences.cornerColorSource : cornerEditMode
315
+ const cornerKey = () => cornerMode() === 'light' ? 'cornerColorLight' : 'cornerColor'
316
+ const cornerCustomKey = () => cornerMode() === 'light' ? 'cornerCustomColorLight' : 'cornerCustomColor'
288
317
  let currentPreferences
289
318
  let currentStatus
290
319
  let currentPersistence = { phase: 'idle', revision: 0 }
@@ -342,6 +371,8 @@ export function createThemeSettingsOverlay({
342
371
  const value = Boolean(next)
343
372
  if (value === isOpen) return isOpen
344
373
  if (!value) {
374
+ closeColorEditor()
375
+ if (currentPreferences) onPreferencePreview(cornerKey(), currentPreferences[cornerKey()])
345
376
  commitParticleTraversalSpeed()
346
377
  commitSessionFlowSpeed()
347
378
  commitSessionFlowColors()
@@ -351,6 +382,8 @@ export function createThemeSettingsOverlay({
351
382
  document.documentElement.toggleAttribute('data-prts-settings-open', isOpen)
352
383
  panel.setAttribute('aria-hidden', String(!isOpen))
353
384
  if (isOpen) {
385
+ cornerEditMode = document.documentElement.getAttribute('data-prts-scheme') === 'light' ? 'light' : 'dark'
386
+ renderState()
354
387
  previousFocus = document.activeElement
355
388
  onOpen()
356
389
  showPanel()
@@ -532,10 +565,18 @@ export function createThemeSettingsOverlay({
532
565
 
533
566
  function renderState() {
534
567
  if (!panel || !currentPreferences) return
568
+ panel.querySelector('[data-prts-corner-modes]').hidden = currentPreferences.cornerColorLinked
569
+ panel.querySelector('[data-prts-corner-sync]').setAttribute('aria-checked', String(currentPreferences.cornerColorLinked))
570
+ for (const mode of panel.querySelectorAll('[data-prts-corner-mode]')) mode.setAttribute('aria-pressed', String(mode.dataset.prtsCornerMode === cornerEditMode))
535
571
  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))
572
+ if (cornerColor) cornerColor.value = currentPreferences[cornerCustomKey()]
573
+ panel.querySelector('[data-prts-corner-preview]')?.style.setProperty('--swatch', currentPreferences[cornerCustomKey()])
574
+ const customButton = panel.querySelector('[data-prts-corner-custom]')
575
+ const customApplied = currentPreferences[cornerKey()] === currentPreferences[cornerCustomKey()]
576
+ customButton.setAttribute('aria-pressed', String(customApplied))
577
+ customButton.title = customApplied ? '自定义颜色已应用,点击编辑' : '选择自定义颜色'
578
+ customButton.querySelector('span').textContent = '自定义'
579
+ for (const preset of panel.querySelectorAll('[data-prts-corner-preset]')) preset.setAttribute('aria-pressed', String(preset.dataset.prtsCornerPreset === currentPreferences[cornerKey()]))
539
580
  for (const toggle of panel.querySelectorAll('[data-prts-setting-switch]')) {
540
581
  const key = toggle.dataset.prtsSettingSwitch
541
582
  const enabled = key === 'cursor' ? currentPreferences.cursor === 'prts'
@@ -628,10 +669,29 @@ export function createThemeSettingsOverlay({
628
669
 
629
670
  function onPanelClick(event) {
630
671
  if (event.target === panel) { setOpen(false); return }
672
+ if (!event.target?.closest?.('[data-prts-corner-picker]')) closeColorEditor()
631
673
  const target = event.target?.closest?.('button')
632
674
  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 }
675
+ if (target.hasAttribute('data-prts-corner-mode')) { onPreferencePreview(cornerKey(), currentPreferences[cornerKey()]); cornerEditMode = target.dataset.prtsCornerMode; renderState(); return }
676
+ if (target.hasAttribute('data-prts-corner-sync')) { if (currentPreferences.cornerColorLinked) cornerEditMode = currentPreferences.cornerColorSource; onPreferenceChange('cornerColorLink', currentPreferences.cornerColorLinked ? false : cornerEditMode); return }
677
+ if (target.hasAttribute('data-prts-color-cancel')) { closeColorEditor(true); return }
678
+ if (target.hasAttribute('data-prts-color-apply')) {
679
+ if (panel.querySelector('[data-prts-color-hex]').getAttribute('aria-invalid') === 'true') return
680
+ onPreferenceChange(cornerCustomKey(), colorDraft); closeColorEditor(true); return
681
+ }
682
+ if (target.hasAttribute('data-prts-corner-custom')) {
683
+ const editor = panel.querySelector('[data-prts-color-editor]')
684
+ if (!editor.hidden) { closeColorEditor(); return }
685
+ colorDraft = currentPreferences[cornerCustomKey()]
686
+ renderColorDraft(); editor.hidden = false
687
+ editor.removeAttribute('data-open-below')
688
+ const scroller = panel.querySelector('[data-prts-theme-settings-scroll]')
689
+ if (editor.getBoundingClientRect().top < (scroller?.getBoundingClientRect().top ?? 0)) editor.setAttribute('data-open-below', '')
690
+ target.setAttribute('aria-expanded', 'true')
691
+ editor.querySelector('[data-prts-color-hex]').focus()
692
+ return
693
+ }
694
+ if (target.hasAttribute('data-prts-corner-preset')) { onPreferenceChange(cornerKey(), target.dataset.prtsCornerPreset); return }
635
695
  if (target.hasAttribute('data-prts-settings-jump')) {
636
696
  selectSection(target.dataset.prtsSettingsJump)
637
697
  return
@@ -670,7 +730,20 @@ export function createThemeSettingsOverlay({
670
730
 
671
731
  function onPanelInput(event) {
672
732
  const target = event.target
673
- if (target?.matches?.('[data-prts-corner-color]')) { onPreferencePreview('cornerColor', target.value); return }
733
+ if (target?.matches?.('[data-prts-color-channel-index], [data-prts-color-hex]')) {
734
+ if (target.hasAttribute('data-prts-color-hex')) {
735
+ const value = target.value.trim()
736
+ if (!/^#?[0-9a-f]{6}$/i.test(value)) { target.setAttribute('aria-invalid', 'true'); return }
737
+ colorDraft = (value.startsWith('#') ? value : '#' + value).toLowerCase()
738
+ } else {
739
+ const channels = [...panel.querySelectorAll('[data-prts-color-channel-index]')].map(input => Number(input.value).toString(16).padStart(2, '0'))
740
+ colorDraft = '#' + channels.join('')
741
+ }
742
+ renderColorDraft()
743
+ onPreferencePreview(cornerKey(), colorDraft)
744
+ return
745
+ }
746
+ if (target?.matches?.('[data-prts-corner-color]')) { onPreferencePreview(cornerKey(), target.value); return }
674
747
  if (target?.matches?.('[data-prts-preview-reply-limit]')) { onPreferenceChange('previewReplyLimit', Number(target.value)); return }
675
748
  if (target?.matches?.('input[data-prts-setting-color]')) {
676
749
  const key = target.dataset.prtsSettingKey
@@ -713,7 +786,7 @@ export function createThemeSettingsOverlay({
713
786
  selectSection(tabs[next].dataset.prtsSettingsJump, true)
714
787
  return
715
788
  }
716
- if (event.key === 'Escape') { event.preventDefault(); setOpen(false); return }
789
+ if (event.key === 'Escape') { event.preventDefault(); if (!panel.querySelector('[data-prts-color-editor]').hidden) { event.stopPropagation(); closeColorEditor(true) } else setOpen(false); return }
717
790
  if (nativeModal || event.key !== 'Tab') return
718
791
  const nodes = focusable()
719
792
  if (!nodes.length) return
@@ -733,7 +806,7 @@ export function createThemeSettingsOverlay({
733
806
  const renderMediaState = () => renderState()
734
807
  const commitFromControl = event => {
735
808
  const target = event.target
736
- if (target?.matches?.('[data-prts-corner-color]')) { if (event.type === 'change') onPreferenceChange('cornerCustomColor', target.value); return }
809
+ if (target?.matches?.('[data-prts-corner-color]')) { if (event.type === 'change') onPreferenceChange(cornerCustomKey(), target.value); return }
737
810
  if (target?.matches?.('input[data-prts-setting-color]')) {
738
811
  if (event.type === 'change' || event.type === 'focusout') commitSessionFlowColor(target.dataset.prtsSettingKey)
739
812
  return
package/src/index.js CHANGED
@@ -12,9 +12,10 @@ export const PACKAGE_ID = '@ash-qw/dsh-theme-prts'
12
12
  export const THEME_PLUGIN_ID = 'dsh-theme-prts'
13
13
  export const inject = ['tools', 'attachments', 'connection', 'webServer']
14
14
 
15
+ // Leave new palette fields absent until normalization can inherit the legacy color.
15
16
  export const Config = Schema.object(Object.fromEntries([
16
17
  ...Object.entries(DEFAULT_PREFERENCES).map(([key, value]) => [key,
17
- (typeof value === 'boolean' ? Schema.boolean() : typeof value === 'number' ? Schema.number() : Schema.string()).default(value).volatile(),
18
+ (typeof value === 'boolean' ? Schema.boolean() : typeof value === 'number' ? Schema.number() : Schema.string()).default(['cornerColorLight', 'cornerCustomColorLight', 'cornerColorSource'].includes(key) ? undefined : value).volatile(),
18
19
  ]),
19
20
  ['preferencesInitialized', Schema.boolean().default(false).volatile()],
20
21
  ]))
@@ -48,7 +48,7 @@ html[data-dsh-prts] [data-prts-session-idle-indicator] {
48
48
  fill: var(--prts-dim);
49
49
  }
50
50
 
51
- html[data-dsh-prts] [data-prts-session-state] [data-prts-session-idle-indicator] {
51
+ html[data-dsh-prts] :is([data-prts-session-state], [data-prts-session-row]:has([data-session-schedule-mark])) [data-prts-session-idle-indicator] {
52
52
  opacity: 0;
53
53
  }
54
54
 
@@ -824,7 +824,7 @@ html[data-dsh-prts] [data-prts-session-summary-popover]:has(> [data-prts-preview
824
824
  }
825
825
 
826
826
  /* Keep different-sized state markers centered on the same axis. */
827
- html[data-dsh-prts] [data-prts-session-row] [data-state]:is([data-state="ongoing"], [data-state="done"], [data-state="warning"], [data-state="error"]) {
827
+ html[data-dsh-prts] [data-prts-session-row] :is([data-state="ongoing"], [data-state="done"], [data-state="warning"], [data-state="error"], [data-session-schedule-mark]) {
828
828
  flex-shrink: 0;
829
829
  margin-left: 20px;
830
830
  }
@@ -889,3 +889,12 @@ html[data-dsh-prts] [data-prts-tool-record] p { display:-webkit-box;-webkit-box-
889
889
 
890
890
  /* Show the baseline immediately; only start the pickup after a deliberate hover. */
891
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; }
892
+
893
+ html[data-dsh-prts] [data-prts-reply-entry][data-prts-reply-complete] { cursor:text; }
894
+
895
+ /* Reserve the cue's width so showing it never changes the truncation measurement. */
896
+ html[data-dsh-prts] [data-prts-reply-entry] > span { display:block;-webkit-line-clamp:unset;max-height:3.2em;color:#b8bec6; }
897
+ html[data-dsh-prts] [data-prts-reply-ellipsis] { flex:0 0 12px;align-self:flex-end;color:#f0e6d6;font:600 14px/1.4 var(--prts-body);visibility:hidden; }
898
+ html[data-dsh-prts] [data-prts-reply-overflow] > [data-prts-reply-ellipsis] { visibility:visible; }
899
+
900
+ html[data-dsh-prts] [data-prts-reply-overflow] > span { color:#f0e6d6; }
@@ -963,7 +963,7 @@ html[data-dsh-prts] [data-prts-row-title] {
963
963
  white-space: nowrap;
964
964
  }
965
965
 
966
- html[data-dsh-prts] [data-prts-session-row] > span:has(> [data-state]) {
966
+ html[data-dsh-prts] [data-prts-session-row] > span:has(> :is([data-state], [data-session-schedule-mark])) {
967
967
  padding-left: 0 !important;
968
968
  }
969
969