@ash-qw/dsh-theme-prts 0.1.99 → 0.1.100

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": "0.1.99",
3
+ "version": "0.1.100",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web",
@@ -59,7 +59,7 @@
59
59
  ],
60
60
  "scripts": {
61
61
  "build": "node scripts/build.mjs",
62
- "test": "node --test tests/*.test.mjs",
62
+ "test": "node --test --test-concurrency=1 tests/*.test.mjs",
63
63
  "test:e2e": "playwright test",
64
64
  "check": "npm run build && npm test",
65
65
  "pack:plugin": "npm run check && npm pack",
@@ -61,6 +61,14 @@ function markPopup(popup, kind) {
61
61
  }
62
62
  }
63
63
 
64
+ function hasVisibleText(element) {
65
+ if (typeof element?.innerText === 'string') return element.innerText.trim().length > 0
66
+ const clone = element?.cloneNode?.(true)
67
+ if (!clone) return false
68
+ for (const hidden of clone.querySelectorAll?.('[hidden], [aria-hidden="true"]') ?? []) hidden.remove()
69
+ return clone.textContent?.trim().length > 0
70
+ }
71
+
64
72
  function fallbackComposerForms(document) {
65
73
  const forms = new Set()
66
74
  for (const root of document?.querySelectorAll?.(FALLBACK_COMPOSER_ROOT_SELECTOR) ?? []) {
@@ -147,12 +155,14 @@ export function createComposerGlassAdapter({ document, window }) {
147
155
  for (const trigger of composer.querySelectorAll('button[aria-haspopup="menu"], button[aria-haspopup="listbox"]')) {
148
156
  const menu = linkedMenu(document, trigger)
149
157
  ?? trigger.parentElement?.querySelector('[role="menu"][aria-busy], [role="listbox"][aria-busy]')
150
- const isModel = trigger.matches('[class*="_7KE1Ra_"]')
151
- || menu?.hasAttribute('aria-busy')
152
158
  const isCommands = trigger.getAttribute('aria-haspopup') === 'listbox'
153
159
  || trigger.getAttribute('aria-label') === 'Commands'
154
160
  || trigger.matches('[class*="uV2eYG_add"]')
155
- const kind = isModel ? 'model' : isCommands ? 'action' : 'action'
161
+ const isModel = !isCommands && (
162
+ menu?.hasAttribute('aria-busy')
163
+ || (trigger.getAttribute('aria-haspopup') === 'menu' && hasVisibleText(trigger))
164
+ )
165
+ const kind = isModel ? 'model' : 'action'
156
166
  mark(trigger, CONTROL_ATTRIBUTE, kind)
157
167
  markPopup(menu, kind)
158
168
  }
@@ -123,9 +123,15 @@ function appendSessionPickup(document, graphic) {
123
123
  'clip-path': `url(#${clipId})`,
124
124
  })
125
125
  for (const strand of ['back', 'core', 'front']) {
126
- lifeline.append(svgNode(document, 'path', {
126
+ const filament = svgNode(document, 'g', {
127
127
  'data-prts-session-lifeline-filament': strand,
128
- }))
128
+ })
129
+ for (const layer of ['halo', 'body', 'core']) {
130
+ filament.append(svgNode(document, 'path', {
131
+ 'data-prts-session-lifeline-layer': layer,
132
+ }))
133
+ }
134
+ lifeline.append(filament)
129
135
  }
130
136
 
131
137
  const pickup = svgNode(document, 'g', {
@@ -193,8 +193,24 @@ export function applyPrtsPlugin(ctx, environment) {
193
193
  persistAndApply(updatePreferenceValue(preferences, key, value))
194
194
  },
195
195
  onPreferencePreview(key, value) {
196
- if (key !== 'particleTraversalSpeed') return
197
- particleField.update(updatePreferenceValue(preferences, key, value))
196
+ if (key === 'particleTraversalSpeed') {
197
+ particleField.update(updatePreferenceValue(preferences, key, value))
198
+ return
199
+ }
200
+ if (key === 'sessionFlowSpeed') {
201
+ const preview = updatePreferenceValue(preferences, key, value)
202
+ document.documentElement.dataset.prtsSessionFlowSpeed = String(preview.sessionFlowSpeed)
203
+ return
204
+ }
205
+ const sessionFlowColorProperties = {
206
+ sessionFlowColorBack: '--prts-session-flow-back',
207
+ sessionFlowColorCore: '--prts-session-flow-core',
208
+ sessionFlowColorFront: '--prts-session-flow-front',
209
+ }
210
+ if (sessionFlowColorProperties[key]) {
211
+ document.documentElement.dataset.prtsSessionFlowPalette = 'custom'
212
+ document.documentElement.style.setProperty(sessionFlowColorProperties[key], String(value))
213
+ }
198
214
  },
199
215
  onVisualReset() {
200
216
  persistAndApply({
@@ -4,9 +4,20 @@ export const PARTICLE_TRAVERSAL_SPEED_MIN = 0
4
4
  export const PARTICLE_TRAVERSAL_SPEED_MAX = 2
5
5
  export const PARTICLE_TRAVERSAL_SPEED_STEP = 0.25
6
6
  export const PARTICLE_TRAVERSAL_SPEED_DEFAULT = 1
7
+ export const SESSION_FLOW_SPEED_MIN = 0.25
8
+ export const SESSION_FLOW_SPEED_MAX = 2
9
+ export const SESSION_FLOW_SPEED_STEP = 0.25
10
+ export const SESSION_FLOW_SPEED_DEFAULT = 1
11
+
12
+ export const SESSION_FLOW_PALETTES = Object.freeze({
13
+ triad: Object.freeze({ back: '#f7ddeb', core: '#65ddb0', front: '#6eb7f3' }),
14
+ rhodes: Object.freeze({ back: '#b9edf4', core: '#5ccddb', front: '#5a9ee6' }),
15
+ amber: Object.freeze({ back: '#ffe6a3', core: '#f0c800', front: '#ee8f42' }),
16
+ alert: Object.freeze({ back: '#ffc0b8', core: '#ee625a', front: '#db5b8c' }),
17
+ })
7
18
 
8
19
  export const DEFAULT_PREFERENCES = Object.freeze({
9
- version: 9,
20
+ version: 12,
10
21
  enabled: false,
11
22
  preset: 'standard-tactical',
12
23
  texture: 'full',
@@ -19,6 +30,12 @@ export const DEFAULT_PREFERENCES = Object.freeze({
19
30
  particlePattern: 'orthogonal',
20
31
  railDefaultHidden: false,
21
32
  sessionFlow: true,
33
+ sessionGlow: true,
34
+ sessionFlowPalette: 'triad',
35
+ sessionFlowColorBack: SESSION_FLOW_PALETTES.triad.back,
36
+ sessionFlowColorCore: SESSION_FLOW_PALETTES.triad.core,
37
+ sessionFlowColorFront: SESSION_FLOW_PALETTES.triad.front,
38
+ sessionFlowSpeed: SESSION_FLOW_SPEED_DEFAULT,
22
39
  conversationStyle: 'native',
23
40
  })
24
41
 
@@ -61,7 +78,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
61
78
  material: Object.freeze(['glass']),
62
79
  accessibility: Object.freeze(['motion', 'bootAnimation']),
63
80
  navigation: Object.freeze(['railDefaultHidden']),
64
- conversation: Object.freeze(['conversationStyle']),
81
+ conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle']),
65
82
  })
66
83
 
67
84
  const TEXTURES = new Set(['off', 'restrained', 'full'])
@@ -70,6 +87,8 @@ const MOTIONS = new Set(['system', 'reduced'])
70
87
  const PARTICLE_DENSITIES = new Set(['sparse', 'light', 'standard', 'dense', 'ultra'])
71
88
  const PRESETS = new Set([...Object.keys(VISUAL_PRESETS), 'custom'])
72
89
  const CONVERSATION_STYLES = new Set(['native', 'deck-chat'])
90
+ const SESSION_FLOW_PALETTE_IDS = new Set([...Object.keys(SESSION_FLOW_PALETTES), 'custom'])
91
+ const SESSION_FLOW_COLOR_KEYS = new Set(['sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront'])
73
92
  const PRESET_LINKED_KEYS = new Set(Object.keys(VISUAL_PRESETS['standard-tactical']))
74
93
 
75
94
 
@@ -99,6 +118,10 @@ function enumOr(value, allowed, fallback) {
99
118
  return allowed.has(value) ? value : fallback
100
119
  }
101
120
 
121
+ function colorOr(value, fallback) {
122
+ return typeof value === 'string' && /^#[\da-f]{6}$/i.test(value) ? value.toLowerCase() : fallback
123
+ }
124
+
102
125
  function particleTraversalSpeedOr(value, fallback) {
103
126
  const numeric = Number(value)
104
127
  if (!Number.isFinite(numeric)) return fallback
@@ -106,6 +129,13 @@ function particleTraversalSpeedOr(value, fallback) {
106
129
  return Math.round(clamped / PARTICLE_TRAVERSAL_SPEED_STEP) * PARTICLE_TRAVERSAL_SPEED_STEP
107
130
  }
108
131
 
132
+ function sessionFlowSpeedOr(value, fallback) {
133
+ const numeric = Number(value)
134
+ if (!Number.isFinite(numeric)) return fallback
135
+ const clamped = Math.min(SESSION_FLOW_SPEED_MAX, Math.max(SESSION_FLOW_SPEED_MIN, numeric))
136
+ return Math.round(clamped / SESSION_FLOW_SPEED_STEP) * SESSION_FLOW_SPEED_STEP
137
+ }
138
+
109
139
  function normalizeGlass(input) {
110
140
  if (input.glassEnabled === false || input.glass === 'off') return 'off'
111
141
  if (input.glass === 'liquid') return 'clear'
@@ -132,8 +162,16 @@ export function normalizePreferences(value) {
132
162
  : input.preset
133
163
  const particlePreset = VISUAL_PRESETS[input.preset] ?? DEFAULT_PREFERENCES
134
164
  const legacyDensities = legacyParticleDensities(input)
165
+ const sessionFlowPalette = enumOr(input.sessionFlowPalette, SESSION_FLOW_PALETTE_IDS, DEFAULT_PREFERENCES.sessionFlowPalette)
166
+ const paletteColors = SESSION_FLOW_PALETTES[sessionFlowPalette]
167
+ const customColors = {
168
+ back: colorOr(input.sessionFlowColorBack, DEFAULT_PREFERENCES.sessionFlowColorBack),
169
+ core: colorOr(input.sessionFlowColorCore, DEFAULT_PREFERENCES.sessionFlowColorCore),
170
+ front: colorOr(input.sessionFlowColorFront, DEFAULT_PREFERENCES.sessionFlowColorFront),
171
+ }
172
+ const sessionFlowColors = paletteColors ?? customColors
135
173
  const normalized = {
136
- version: 9,
174
+ version: 12,
137
175
  enabled: booleanOr(input.enabled, DEFAULT_PREFERENCES.enabled),
138
176
  preset: enumOr(migratedPreset, PRESETS, DEFAULT_PREFERENCES.preset),
139
177
  texture: enumOr(input.texture, TEXTURES, DEFAULT_PREFERENCES.texture),
@@ -149,6 +187,12 @@ export function normalizePreferences(value) {
149
187
  particlePattern: 'orthogonal',
150
188
  railDefaultHidden: booleanOr(input.railDefaultHidden, DEFAULT_PREFERENCES.railDefaultHidden),
151
189
  sessionFlow: booleanOr(input.sessionFlow, DEFAULT_PREFERENCES.sessionFlow),
190
+ sessionGlow: booleanOr(input.sessionGlow, DEFAULT_PREFERENCES.sessionGlow),
191
+ sessionFlowPalette,
192
+ sessionFlowColorBack: sessionFlowColors.back,
193
+ sessionFlowColorCore: sessionFlowColors.core,
194
+ sessionFlowColorFront: sessionFlowColors.front,
195
+ sessionFlowSpeed: sessionFlowSpeedOr(input.sessionFlowSpeed, DEFAULT_PREFERENCES.sessionFlowSpeed),
152
196
  conversationStyle: enumOr(input.conversationStyle, CONVERSATION_STYLES, DEFAULT_PREFERENCES.conversationStyle),
153
197
  }
154
198
  normalized.preset = matchedVisualPreset(normalized)
@@ -175,6 +219,26 @@ export function updatePreferenceValue(value, key, nextValue) {
175
219
  if (key === 'preset') return applyVisualPreset(value, nextValue)
176
220
  if (key === 'particleDetail') return applyParticleDetail(value, nextValue)
177
221
  const current = normalizePreferences(value)
222
+ if (key === 'sessionFlowPalette') {
223
+ const palette = enumOr(nextValue, SESSION_FLOW_PALETTE_IDS, current.sessionFlowPalette)
224
+ const colors = SESSION_FLOW_PALETTES[palette]
225
+ return normalizePreferences({
226
+ ...current,
227
+ sessionFlowPalette: palette,
228
+ ...(colors ? {
229
+ sessionFlowColorBack: colors.back,
230
+ sessionFlowColorCore: colors.core,
231
+ sessionFlowColorFront: colors.front,
232
+ } : {}),
233
+ })
234
+ }
235
+ if (SESSION_FLOW_COLOR_KEYS.has(key)) {
236
+ return normalizePreferences({
237
+ ...current,
238
+ sessionFlowPalette: 'custom',
239
+ [key]: colorOr(nextValue, current[key]),
240
+ })
241
+ }
178
242
  if (key === 'conversationParticleDensity' || key === 'heroParticleDensity') {
179
243
  const density = enumOr(nextValue, PARTICLE_DENSITIES, current[key])
180
244
  return normalizePreferences({ ...current, [key]: density })
@@ -196,7 +260,7 @@ export function loadPreferences(storage) {
196
260
  const raw = storage?.getItem(PRTS_STORAGE_KEY)
197
261
  if (raw === null || raw === undefined) return { ...DEFAULT_PREFERENCES }
198
262
  const parsed = JSON.parse(raw)
199
- if (![1, 2, 3, 4, 5, 6, 7, 8, 9].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
263
+ if (![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
200
264
  return normalizePreferences(parsed)
201
265
  } catch {
202
266
  return { ...DEFAULT_PREFERENCES }
@@ -1,6 +1,7 @@
1
1
  const HOVER_SETTLE = 180
2
2
  const STATIC_TIME = 2.4
3
3
  const POINT_COUNT = 9
4
+ const DEFAULT_FLOW_SPEED = 1
4
5
 
5
6
  function clamp(value, min, max) {
6
7
  return Math.min(max, Math.max(min, value))
@@ -25,6 +26,15 @@ function filamentNodes(lifeline) {
25
26
  return Array.from(lifeline.querySelectorAll?.('[data-prts-session-lifeline-filament]') ?? [])
26
27
  }
27
28
 
29
+ function setFilamentPath(filament, path) {
30
+ const layers = Array.from(filament?.querySelectorAll?.('[data-prts-session-lifeline-layer]') ?? [])
31
+ if (layers.length === 0) {
32
+ filament?.setAttribute?.('d', path)
33
+ return
34
+ }
35
+ layers.forEach(layer => layer.setAttribute('d', path))
36
+ }
37
+
28
38
  function createSmoothPath(points) {
29
39
  if (points.length < 2) return ''
30
40
  const commands = [`M ${number(points[0].x)} ${number(points[0].y)}`]
@@ -91,7 +101,7 @@ export function configureSessionLifeline(lifeline, { start, end, baseline, lift
91
101
  lifeline.setAttribute('data-prts-lifeline-lift', String(number(lift)))
92
102
  filamentNodes(lifeline).forEach((filament, index, filaments) => {
93
103
  const strand = filaments.length === 1 ? 0 : index - (filaments.length - 1) / 2
94
- filament.setAttribute('d', createSessionLifelinePath(geometry, STATIC_TIME, 1, strand))
104
+ setFilamentPath(filament, createSessionLifelinePath(geometry, STATIC_TIME, 1, strand))
95
105
  })
96
106
  return true
97
107
  }
@@ -119,7 +129,7 @@ export function createSessionLifelineAnimator({ document, window }) {
119
129
  if (!geometry || filaments.length === 0) return false
120
130
  filaments.forEach((filament, index) => {
121
131
  const strand = filaments.length === 1 ? 0 : index - (filaments.length - 1) / 2
122
- filament.setAttribute('d', createSessionLifelinePath(geometry, flowTime, strength, strand))
132
+ setFilamentPath(filament, createSessionLifelinePath(geometry, flowTime, strength, strand))
123
133
  })
124
134
  return true
125
135
  }
@@ -136,6 +146,11 @@ export function createSessionLifelineAnimator({ document, window }) {
136
146
  )
137
147
  }
138
148
 
149
+ function currentFlowSpeed() {
150
+ const value = Number(document?.documentElement?.dataset?.prtsSessionFlowSpeed)
151
+ return Number.isFinite(value) ? clamp(value, .25, 2) : DEFAULT_FLOW_SPEED
152
+ }
153
+
139
154
  function cancel() {
140
155
  if (frame !== undefined) cancelFrame?.(frame)
141
156
  frame = undefined
@@ -161,7 +176,7 @@ export function createSessionLifelineAnimator({ document, window }) {
161
176
  if (lastTime === undefined) lastTime = now
162
177
  const elapsed = Math.min(100, Math.max(0, now - lastTime))
163
178
  lastTime = now
164
- flowTime += elapsed / 1000
179
+ flowTime += elapsed / 1000 * currentFlowSpeed()
165
180
  const target = hovered ? 0 : 1
166
181
  if (strength !== target) {
167
182
  const step = elapsed / HOVER_SETTLE
@@ -212,7 +227,7 @@ export function createSessionLifelineAnimator({ document, window }) {
212
227
  motionObserver = new MotionObserver(syncMotion)
213
228
  motionObserver.observe(document.documentElement, {
214
229
  attributes: true,
215
- attributeFilter: ['data-prts-motion', 'data-prts-session-flow'],
230
+ attributeFilter: ['data-prts-motion', 'data-prts-session-flow', 'data-prts-session-flow-speed'],
216
231
  })
217
232
  }
218
233
  }
@@ -8,8 +8,16 @@ const ROOT_ATTRIBUTES = [
8
8
  'data-prts-motion',
9
9
  'data-prts-particle-pattern',
10
10
  'data-prts-session-flow',
11
+ 'data-prts-session-glow',
12
+ 'data-prts-session-flow-palette',
13
+ 'data-prts-session-flow-speed',
11
14
  'data-prts-conversation-style',
12
15
  ]
16
+ const SESSION_FLOW_COLOR_PROPERTIES = {
17
+ '--prts-session-flow-back': 'sessionFlowColorBack',
18
+ '--prts-session-flow-core': 'sessionFlowColorCore',
19
+ '--prts-session-flow-front': 'sessionFlowColorFront',
20
+ }
13
21
 
14
22
  const TRANSITION_ATTRIBUTE = 'data-prts-scheme-transition'
15
23
  const TRANSITION_MODE_ATTRIBUTE = 'data-prts-scheme-transition-mode'
@@ -102,6 +110,7 @@ export function createThemeController({ document, window, cssText, service, onTr
102
110
 
103
111
  function clearThemeState() {
104
112
  for (const attribute of ROOT_ATTRIBUTES) root.removeAttribute(attribute)
113
+ for (const property of Object.keys(SESSION_FLOW_COLOR_PROPERTIES)) root.style.removeProperty(property)
105
114
  clearTransitionState()
106
115
  }
107
116
 
@@ -718,6 +727,10 @@ export function createThemeController({ document, window, cssText, service, onTr
718
727
  root.dataset.prtsMotion = preferences.motion
719
728
  root.dataset.prtsParticlePattern = 'orthogonal'
720
729
  root.dataset.prtsSessionFlow = preferences.sessionFlow ? 'on' : 'off'
730
+ root.dataset.prtsSessionGlow = preferences.sessionGlow ? 'on' : 'off'
731
+ root.dataset.prtsSessionFlowPalette = preferences.sessionFlowPalette
732
+ root.dataset.prtsSessionFlowSpeed = String(preferences.sessionFlowSpeed)
733
+ for (const [property, key] of Object.entries(SESSION_FLOW_COLOR_PROPERTIES)) root.style.setProperty(property, preferences[key])
721
734
  root.dataset.prtsConversationStyle = preferences.conversationStyle
722
735
  refresh()
723
736
  },
@@ -8,8 +8,16 @@ const ROOT_ATTRIBUTES = [
8
8
  'data-prts-motion',
9
9
  'data-prts-particle-pattern',
10
10
  'data-prts-session-flow',
11
+ 'data-prts-session-glow',
12
+ 'data-prts-session-flow-palette',
13
+ 'data-prts-session-flow-speed',
11
14
  'data-prts-conversation-style',
12
15
  ]
16
+ const SESSION_FLOW_COLOR_PROPERTIES = {
17
+ '--prts-session-flow-back': 'sessionFlowColorBack',
18
+ '--prts-session-flow-core': 'sessionFlowColorCore',
19
+ '--prts-session-flow-front': 'sessionFlowColorFront',
20
+ }
13
21
 
14
22
  function themeIdentity(value) {
15
23
  if (typeof value === 'string') return value
@@ -37,6 +45,7 @@ export function createThemeController({ document, window, cssText, service }) {
37
45
 
38
46
  function clearThemeState() {
39
47
  for (const attribute of ROOT_ATTRIBUTES) root.removeAttribute(attribute)
48
+ for (const property of Object.keys(SESSION_FLOW_COLOR_PROPERTIES)) root.style.removeProperty(property)
40
49
  }
41
50
 
42
51
  function removeOwnedState() {
@@ -111,6 +120,10 @@ export function createThemeController({ document, window, cssText, service }) {
111
120
  root.dataset.prtsMotion = preferences.motion
112
121
  root.dataset.prtsParticlePattern = 'orthogonal'
113
122
  root.dataset.prtsSessionFlow = preferences.sessionFlow ? 'on' : 'off'
123
+ root.dataset.prtsSessionGlow = preferences.sessionGlow ? 'on' : 'off'
124
+ root.dataset.prtsSessionFlowPalette = preferences.sessionFlowPalette
125
+ root.dataset.prtsSessionFlowSpeed = String(preferences.sessionFlowSpeed)
126
+ for (const [property, key] of Object.entries(SESSION_FLOW_COLOR_PROPERTIES)) root.style.setProperty(property, preferences[key])
114
127
  root.dataset.prtsConversationStyle = preferences.conversationStyle
115
128
  refresh()
116
129
  },