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

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.98",
3
+ "version": "0.1.99",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web",
@@ -1,4 +1,5 @@
1
1
  import { createFacilityPath } from './facility-geometry.js'
2
+ import { configureSessionLifeline } from './session-lifeline.js'
2
3
 
3
4
  const SVG_NS = 'http://www.w3.org/2000/svg'
4
5
  const SILHOUETTE_TRACK_WIDTH = 512
@@ -117,6 +118,16 @@ function appendSessionPickup(document, graphic) {
117
118
  clip.append(svgNode(document, 'path', { 'data-prts-facility-clip': '' }))
118
119
  defs.append(clip)
119
120
 
121
+ const lifeline = svgNode(document, 'g', {
122
+ 'data-prts-session-lifeline': '',
123
+ 'clip-path': `url(#${clipId})`,
124
+ })
125
+ for (const strand of ['back', 'core', 'front']) {
126
+ lifeline.append(svgNode(document, 'path', {
127
+ 'data-prts-session-lifeline-filament': strand,
128
+ }))
129
+ }
130
+
120
131
  const pickup = svgNode(document, 'g', {
121
132
  'data-prts-session-pickup': '',
122
133
  'clip-path': `url(#${clipId})`,
@@ -146,9 +157,22 @@ function appendSessionPickup(document, graphic) {
146
157
  })
147
158
 
148
159
  graphic.insertBefore(defs, graphic.firstChild)
160
+ graphic.insertBefore(lifeline, graphic.querySelector('[data-prts-facility-layer="outline"]'))
149
161
  graphic.insertBefore(pickup, graphic.querySelector('[data-prts-facility-layer="outline"]'))
150
162
  }
151
163
 
164
+ function clamp(value, min, max) {
165
+ return Math.min(max, Math.max(min, value))
166
+ }
167
+
168
+ function sessionLifelineGeometry(width, height) {
169
+ const start = clamp(width * .04, 8, 12)
170
+ const end = Math.max(start + 1, width - 8)
171
+ const baseline = height * .56
172
+ const lift = clamp(height * .35, 10, 14)
173
+ return { start, end, baseline, lift }
174
+ }
175
+
152
176
  export function ensureFacilityGraphic(document, owner, { kind, topNotch = false, texture = 'none' } = {}) {
153
177
  if (!owner) return null
154
178
  let graphic = owner.querySelector?.(':scope > svg[data-prts-facility-svg]')
@@ -200,6 +224,8 @@ export function renderFacilityGraphic(owner, { width, height } = {}) {
200
224
  }
201
225
 
202
226
  const size = `${Number(width.toFixed(3))}x${Number(height.toFixed(3))}`
227
+ const lifeline = graphic.querySelector('[data-prts-session-lifeline]')
228
+ const lifelineGeometry = lifeline ? sessionLifelineGeometry(width, height) : undefined
203
229
  if (owner.getAttribute('data-prts-facility-size') !== size) {
204
230
  graphic.setAttribute('viewBox', `0 0 ${Number(width.toFixed(3))} ${Number(height.toFixed(3))}`)
205
231
  for (const layer of graphic.querySelectorAll('[data-prts-facility-layer], [data-prts-facility-clip]')) layer.setAttribute('d', path)
@@ -223,6 +249,9 @@ export function renderFacilityGraphic(owner, { width, height } = {}) {
223
249
  bar.setAttribute('y', String(Number((center - barHeight / 2).toFixed(3))))
224
250
  }
225
251
  }
252
+ if (lifeline) {
253
+ configureSessionLifeline(lifeline, lifelineGeometry)
254
+ }
226
255
  owner.setAttribute('data-prts-facility-size', size)
227
256
  }
228
257
  setReady(owner, true)
@@ -18,6 +18,7 @@ export const DEFAULT_PREFERENCES = Object.freeze({
18
18
  particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
19
19
  particlePattern: 'orthogonal',
20
20
  railDefaultHidden: false,
21
+ sessionFlow: true,
21
22
  conversationStyle: 'native',
22
23
  })
23
24
 
@@ -147,6 +148,7 @@ export function normalizePreferences(value) {
147
148
  ),
148
149
  particlePattern: 'orthogonal',
149
150
  railDefaultHidden: booleanOr(input.railDefaultHidden, DEFAULT_PREFERENCES.railDefaultHidden),
151
+ sessionFlow: booleanOr(input.sessionFlow, DEFAULT_PREFERENCES.sessionFlow),
150
152
  conversationStyle: enumOr(input.conversationStyle, CONVERSATION_STYLES, DEFAULT_PREFERENCES.conversationStyle),
151
153
  }
152
154
  normalized.preset = matchedVisualPreset(normalized)
@@ -0,0 +1,255 @@
1
+ const HOVER_SETTLE = 180
2
+ const STATIC_TIME = 2.4
3
+ const POINT_COUNT = 9
4
+
5
+ function clamp(value, min, max) {
6
+ return Math.min(max, Math.max(min, value))
7
+ }
8
+
9
+ function number(value) {
10
+ return Number(value.toFixed(3))
11
+ }
12
+
13
+ function readGeometry(lifeline) {
14
+ const start = Number(lifeline?.getAttribute?.('data-prts-lifeline-start'))
15
+ const end = Number(lifeline?.getAttribute?.('data-prts-lifeline-end'))
16
+ const baseline = Number(lifeline?.getAttribute?.('data-prts-lifeline-baseline'))
17
+ const lift = Number(lifeline?.getAttribute?.('data-prts-lifeline-lift'))
18
+ if (![start, end, baseline, lift].every(Number.isFinite) || end <= start || lift <= 0) return null
19
+ return { start, end, baseline, lift }
20
+ }
21
+
22
+ function filamentNodes(lifeline) {
23
+ if (!lifeline) return []
24
+ if (lifeline.matches?.('[data-prts-session-lifeline-filament]')) return [lifeline]
25
+ return Array.from(lifeline.querySelectorAll?.('[data-prts-session-lifeline-filament]') ?? [])
26
+ }
27
+
28
+ function createSmoothPath(points) {
29
+ if (points.length < 2) return ''
30
+ const commands = [`M ${number(points[0].x)} ${number(points[0].y)}`]
31
+ for (let index = 0; index < points.length - 1; index += 1) {
32
+ const previous = points[Math.max(0, index - 1)]
33
+ const current = points[index]
34
+ const next = points[index + 1]
35
+ const following = points[Math.min(points.length - 1, index + 2)]
36
+ const firstControl = {
37
+ x: current.x + (next.x - previous.x) / 6,
38
+ y: current.y + (next.y - previous.y) / 6,
39
+ }
40
+ const secondControl = {
41
+ x: next.x - (following.x - current.x) / 6,
42
+ y: next.y - (following.y - current.y) / 6,
43
+ }
44
+ commands.push([
45
+ 'C',
46
+ number(firstControl.x),
47
+ number(firstControl.y),
48
+ number(secondControl.x),
49
+ number(secondControl.y),
50
+ number(next.x),
51
+ number(next.y),
52
+ ].join(' '))
53
+ }
54
+ return commands.join(' ')
55
+ }
56
+
57
+ function sampleFlow(u, time, strand) {
58
+ const edgeEnvelope = Math.sin(Math.PI * u) ** .88
59
+ const slowTurn = Math.sin(time * .43 + u * Math.PI * 1.16 + Math.sin(time * .13) * .48)
60
+ const crossCurrent = Math.sin(time * -.29 + u * Math.PI * 2.42 + .7)
61
+ const fineCurrent = Math.sin(time * .19 + u * Math.PI * 3.76 - .9)
62
+ const sharedFlow = slowTurn * .52 + crossCurrent * .24 + fineCurrent * .08
63
+
64
+ const fan = Math.sin(Math.PI * u) ** 1.35
65
+ const separation = strand * (.15 + Math.sin(time * .31 + u * Math.PI * 1.48 + strand * .8) * .1)
66
+ const independentDrift = Math.sin(time * (.36 + strand * .021) + u * Math.PI * 2.18 + strand * 1.35) * .055
67
+ return edgeEnvelope * clamp(sharedFlow + fan * separation + independentDrift, -.9, .9)
68
+ }
69
+
70
+ export function createSessionLifelinePath(geometry, time = STATIC_TIME, strength = 1, strand = 0) {
71
+ const { start, end, baseline, lift } = geometry
72
+ const span = end - start
73
+ const tension = clamp(strength, 0, 1)
74
+ const points = []
75
+ for (let index = 0; index < POINT_COUNT; index += 1) {
76
+ const u = index / (POINT_COUNT - 1)
77
+ points.push({
78
+ x: start + span * u,
79
+ y: baseline - lift * sampleFlow(u, time, strand) * tension,
80
+ })
81
+ }
82
+ return createSmoothPath(points)
83
+ }
84
+
85
+ export function configureSessionLifeline(lifeline, { start, end, baseline, lift }) {
86
+ if (!lifeline) return false
87
+ const geometry = { start, end, baseline, lift }
88
+ lifeline.setAttribute('data-prts-lifeline-start', String(number(start)))
89
+ lifeline.setAttribute('data-prts-lifeline-end', String(number(end)))
90
+ lifeline.setAttribute('data-prts-lifeline-baseline', String(number(baseline)))
91
+ lifeline.setAttribute('data-prts-lifeline-lift', String(number(lift)))
92
+ filamentNodes(lifeline).forEach((filament, index, filaments) => {
93
+ const strand = filaments.length === 1 ? 0 : index - (filaments.length - 1) / 2
94
+ filament.setAttribute('d', createSessionLifelinePath(geometry, STATIC_TIME, 1, strand))
95
+ })
96
+ return true
97
+ }
98
+
99
+ export function createSessionLifelineAnimator({ document, window }) {
100
+ const requestFrame = window?.requestAnimationFrame?.bind(window)
101
+ ?? (callback => window?.setTimeout?.(() => callback(Date.now()), 1000 / 60))
102
+ const cancelFrame = window?.cancelAnimationFrame?.bind(window)
103
+ ?? window?.clearTimeout?.bind(window)
104
+ const reducedMotion = window?.matchMedia?.('(prefers-reduced-motion: reduce)')
105
+ const MotionObserver = window?.MutationObserver
106
+
107
+ let frame
108
+ let lastTime
109
+ let flowTime = STATIC_TIME
110
+ let strength = 1
111
+ let row
112
+ let lifeline
113
+ let filaments = []
114
+ let hovered = false
115
+ let motionObserver
116
+
117
+ function render() {
118
+ const geometry = readGeometry(lifeline)
119
+ if (!geometry || filaments.length === 0) return false
120
+ filaments.forEach((filament, index) => {
121
+ const strand = filaments.length === 1 ? 0 : index - (filaments.length - 1) / 2
122
+ filament.setAttribute('d', createSessionLifelinePath(geometry, flowTime, strength, strand))
123
+ })
124
+ return true
125
+ }
126
+
127
+ function canAnimate() {
128
+ return Boolean(
129
+ row?.isConnected
130
+ && lifeline?.isConnected
131
+ && row.getAttribute('aria-selected') === 'true'
132
+ && document?.visibilityState !== 'hidden'
133
+ && document?.documentElement?.dataset?.prtsSessionFlow !== 'off'
134
+ && document?.documentElement?.dataset?.prtsMotion !== 'reduced'
135
+ && !reducedMotion?.matches
136
+ )
137
+ }
138
+
139
+ function cancel() {
140
+ if (frame !== undefined) cancelFrame?.(frame)
141
+ frame = undefined
142
+ lastTime = undefined
143
+ }
144
+
145
+ function needsFrame() {
146
+ return canAnimate() && (!hovered || strength > 0)
147
+ }
148
+
149
+ function schedule() {
150
+ if (frame !== undefined || !needsFrame()) return
151
+ frame = requestFrame?.(tick)
152
+ }
153
+
154
+ function tick(time) {
155
+ frame = undefined
156
+ if (!canAnimate()) {
157
+ lastTime = undefined
158
+ return
159
+ }
160
+ const now = Number.isFinite(time) ? time : Date.now()
161
+ if (lastTime === undefined) lastTime = now
162
+ const elapsed = Math.min(100, Math.max(0, now - lastTime))
163
+ lastTime = now
164
+ flowTime += elapsed / 1000
165
+ const target = hovered ? 0 : 1
166
+ if (strength !== target) {
167
+ const step = elapsed / HOVER_SETTLE
168
+ strength = target > strength
169
+ ? Math.min(target, strength + step)
170
+ : Math.max(target, strength - step)
171
+ }
172
+ render()
173
+ schedule()
174
+ }
175
+
176
+ function motionReduced() {
177
+ return document?.documentElement?.dataset?.prtsMotion === 'reduced' || reducedMotion?.matches
178
+ }
179
+
180
+ function syncMotion() {
181
+ if (canAnimate()) {
182
+ schedule()
183
+ return
184
+ }
185
+ cancel()
186
+ if (lifeline && motionReduced()) {
187
+ flowTime = STATIC_TIME
188
+ strength = 1
189
+ render()
190
+ }
191
+ }
192
+
193
+ function onPointerEnter() {
194
+ hovered = true
195
+ syncMotion()
196
+ }
197
+
198
+ function onPointerLeave() {
199
+ hovered = false
200
+ syncMotion()
201
+ }
202
+
203
+ function detachRow() {
204
+ row?.removeEventListener?.('pointerenter', onPointerEnter)
205
+ row?.removeEventListener?.('pointerleave', onPointerLeave)
206
+ }
207
+
208
+ function attachObservers() {
209
+ document?.addEventListener?.('visibilitychange', syncMotion)
210
+ reducedMotion?.addEventListener?.('change', syncMotion)
211
+ if (!motionObserver && typeof MotionObserver === 'function' && document?.documentElement) {
212
+ motionObserver = new MotionObserver(syncMotion)
213
+ motionObserver.observe(document.documentElement, {
214
+ attributes: true,
215
+ attributeFilter: ['data-prts-motion', 'data-prts-session-flow'],
216
+ })
217
+ }
218
+ }
219
+
220
+ return {
221
+ start() {
222
+ attachObservers()
223
+ syncMotion()
224
+ },
225
+ setTarget(nextRow, nextLifeline) {
226
+ if (row === nextRow && lifeline === nextLifeline) {
227
+ syncMotion()
228
+ return
229
+ }
230
+ cancel()
231
+ detachRow()
232
+ row = nextRow
233
+ lifeline = nextLifeline
234
+ filaments = filamentNodes(lifeline)
235
+ hovered = Boolean(row?.matches?.(':hover'))
236
+ flowTime = STATIC_TIME
237
+ strength = hovered ? 0 : 1
238
+ row?.addEventListener?.('pointerenter', onPointerEnter)
239
+ row?.addEventListener?.('pointerleave', onPointerLeave)
240
+ render()
241
+ syncMotion()
242
+ },
243
+ dispose() {
244
+ cancel()
245
+ detachRow()
246
+ row = undefined
247
+ lifeline = undefined
248
+ filaments = []
249
+ motionObserver?.disconnect()
250
+ motionObserver = undefined
251
+ document?.removeEventListener?.('visibilitychange', syncMotion)
252
+ reducedMotion?.removeEventListener?.('change', syncMotion)
253
+ },
254
+ }
255
+ }
@@ -1,4 +1,5 @@
1
1
  import { ensureFacilityGraphic, ensureFacilityTextureDefs, renderFacilityGraphic } from './facility-vector.js'
2
+ import { createSessionLifelineAnimator } from './session-lifeline.js'
2
3
 
3
4
  const OWNED_ATTRIBUTES = [
4
5
  'data-prts-row-title',
@@ -128,6 +129,7 @@ export function createSidebarControlAdapter({ document, window }) {
128
129
  let vectorOwners = new Set()
129
130
  const dirtyVectors = new Map()
130
131
  const vectorSizes = new WeakMap()
132
+ const lifelineAnimator = createSessionLifelineAnimator({ document, window })
131
133
 
132
134
  function scheduleVectors() {
133
135
  if (resizeFrame !== undefined) return
@@ -191,6 +193,8 @@ export function createSidebarControlAdapter({ document, window }) {
191
193
  const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
192
194
  const next = new Set()
193
195
  const nextVectorOwners = new Set()
196
+ let activeLifelineRow
197
+ let activeLifeline
194
198
  if (root) {
195
199
  for (const projection of root.querySelectorAll('[data-prts-owned-projection]')) projection.remove()
196
200
  for (const row of root.querySelectorAll('div[role="treeitem"][aria-expanded]')) {
@@ -222,6 +226,10 @@ export function createSidebarControlAdapter({ document, window }) {
222
226
  const spine = ensureFacilitySpine(document, row, 'session', next)
223
227
  registerVector(face, { kind: 'face', topNotch: true, texture: 'pickup' }, next, nextVectorOwners)
224
228
  registerVector(spine, { kind: 'spine' }, next, nextVectorOwners)
229
+ if (!activeLifelineRow && row.getAttribute('aria-selected') === 'true') {
230
+ activeLifelineRow = row
231
+ activeLifeline = face.querySelector('[data-prts-session-lifeline]')
232
+ }
225
233
  markSidebarNode(row, 'data-prts-session-row', next)
226
234
  row.dataset.prtsSessionIndex = sessionIndex
227
235
  const title = resolveRowTitle(row, 'session', controls.actions)
@@ -236,6 +244,7 @@ export function createSidebarControlAdapter({ document, window }) {
236
244
  registerVector(controls.menu, { kind: 'button' }, next, nextVectorOwners)
237
245
  }
238
246
  }
247
+ lifelineAnimator.setTarget(activeLifelineRow, activeLifeline)
239
248
  for (const node of owned) {
240
249
  if (next.has(node)) continue
241
250
  if (node.hasAttribute?.('data-prts-owned-projection')
@@ -252,6 +261,7 @@ export function createSidebarControlAdapter({ document, window }) {
252
261
  }
253
262
 
254
263
  function clear() {
264
+ lifelineAnimator.setTarget(undefined, undefined)
255
265
  for (const node of document?.querySelectorAll?.('[data-prts-owned-projection], [data-prts-owned-facility], [data-prts-owned-facility-spine], [data-prts-owned-facility-vector], [data-prts-owned-facility-defs]') ?? []) node.remove()
256
266
  for (const node of owned) {
257
267
  for (const attribute of OWNED_ATTRIBUTES) node.removeAttribute?.(attribute)
@@ -267,6 +277,7 @@ export function createSidebarControlAdapter({ document, window }) {
267
277
  return {
268
278
  start() {
269
279
  if (!document) return
280
+ lifelineAnimator.start()
270
281
  ensureFacilityTextureDefs(document)
271
282
  const ResizeObserver = window?.ResizeObserver
272
283
  if (!resizeObserver && typeof ResizeObserver === 'function') {
@@ -311,6 +322,7 @@ export function createSidebarControlAdapter({ document, window }) {
311
322
  resizeFrame = undefined
312
323
  if (resizeTimer !== undefined) window?.clearTimeout?.(resizeTimer)
313
324
  resizeTimer = undefined
325
+ lifelineAnimator.dispose()
314
326
  clear()
315
327
  },
316
328
  }
@@ -7,6 +7,7 @@ const ROOT_ATTRIBUTES = [
7
7
  'data-prts-glass',
8
8
  'data-prts-motion',
9
9
  'data-prts-particle-pattern',
10
+ 'data-prts-session-flow',
10
11
  'data-prts-conversation-style',
11
12
  ]
12
13
 
@@ -716,6 +717,7 @@ export function createThemeController({ document, window, cssText, service, onTr
716
717
  root.dataset.prtsGlass = preferences.glass
717
718
  root.dataset.prtsMotion = preferences.motion
718
719
  root.dataset.prtsParticlePattern = 'orthogonal'
720
+ root.dataset.prtsSessionFlow = preferences.sessionFlow ? 'on' : 'off'
719
721
  root.dataset.prtsConversationStyle = preferences.conversationStyle
720
722
  refresh()
721
723
  },
@@ -7,6 +7,7 @@ const ROOT_ATTRIBUTES = [
7
7
  'data-prts-glass',
8
8
  'data-prts-motion',
9
9
  'data-prts-particle-pattern',
10
+ 'data-prts-session-flow',
10
11
  'data-prts-conversation-style',
11
12
  ]
12
13
 
@@ -109,6 +110,7 @@ export function createThemeController({ document, window, cssText, service }) {
109
110
  root.dataset.prtsGlass = preferences.glass
110
111
  root.dataset.prtsMotion = preferences.motion
111
112
  root.dataset.prtsParticlePattern = 'orthogonal'
113
+ root.dataset.prtsSessionFlow = preferences.sessionFlow ? 'on' : 'off'
112
114
  root.dataset.prtsConversationStyle = preferences.conversationStyle
113
115
  refresh()
114
116
  },
@@ -19,6 +19,7 @@ const COMMON_CONTROLS = [
19
19
  ['texture', '环境底纹', [['off', '关闭'], ['restrained', '克制'], ['full', '完整']]],
20
20
  ['glass', '玻璃材质', [['off', '关闭'], ['soft', '柔和'], ['standard', '标准'], ['clear', '清晰']]],
21
21
  ['motion', '动态效果', [['system', '跟随系统'], ['reduced', '减少']]],
22
+ ['sessionFlow', '当前会话流线', [['false', '关闭'], ['true', '开启']]],
22
23
  ['conversationStyle', '会话主题', [['native', '原有'], ['deck-chat', '通讯链路']]],
23
24
  ]
24
25
 
@@ -37,6 +38,8 @@ function settingButtons(key, label, options) {
37
38
  ? '<small data-prts-setting-note>在任意窗口宽度下默认收起 P.R.T.S. 导航,可从左侧按钮临时展开</small>'
38
39
  : key === 'conversationStyle'
39
40
  ? '<small data-prts-setting-note>立即切换会话背景、头像与消息气泡</small>'
41
+ : key === 'sessionFlow'
42
+ ? '<small data-prts-setting-note>仅控制当前会话卡片的三色流线</small>'
40
43
  : ''
41
44
  const sample = value => key === 'texture' || key === 'glass'
42
45
  ? `<i data-prts-option-sample="${key}" data-prts-option-sample-value="${value}" aria-hidden="true"></i>`
@@ -281,7 +284,11 @@ export function createThemeSettingsOverlay({
281
284
  const particleDetail = resolveParticleDetail(currentPreferences)
282
285
  for (const button of panel.querySelectorAll('button[data-prts-setting-key]')) {
283
286
  const key = button.dataset.prtsSettingKey
284
- const actual = key === 'particleDetail' ? particleDetail : currentPreferences[key]
287
+ const actual = key === 'particleDetail'
288
+ ? particleDetail
289
+ : key === 'sessionFlow'
290
+ ? currentPreferences.sessionFlow !== false
291
+ : currentPreferences[key]
285
292
  const selected = String(actual) === button.dataset.prtsSettingValue
286
293
  button.classList.toggle('is-selected', selected)
287
294
  button.setAttribute('aria-pressed', String(selected))
@@ -141,6 +141,71 @@ html[data-dsh-prts] [data-prts-session-pickup] {
141
141
  pointer-events: none;
142
142
  }
143
143
 
144
+ html[data-dsh-prts] [data-prts-session-lifeline] {
145
+ --prts-lifeline-pink: #f7ddeb;
146
+ --prts-lifeline-green: #65ddb0;
147
+ --prts-lifeline-blue: #6eb7f3;
148
+ opacity: 0;
149
+ transition: opacity 160ms ease;
150
+ pointer-events: none;
151
+ }
152
+
153
+ html[data-dsh-prts][data-prts-scheme="light"] [data-prts-session-lifeline] {
154
+ --prts-lifeline-pink: #c36f99;
155
+ --prts-lifeline-green: #218d6b;
156
+ --prts-lifeline-blue: #357ebc;
157
+ }
158
+
159
+ html[data-dsh-prts] [data-prts-session-lifeline-filament] {
160
+ fill: none;
161
+ stroke: currentColor;
162
+ stroke-width: .8;
163
+ stroke-linecap: round;
164
+ stroke-linejoin: round;
165
+ vector-effect: non-scaling-stroke;
166
+ shape-rendering: geometricPrecision;
167
+ }
168
+
169
+ html[data-dsh-prts] [data-prts-session-lifeline-filament="back"] {
170
+ stroke: var(--prts-lifeline-pink);
171
+ opacity: .74;
172
+ stroke-width: .85;
173
+ }
174
+
175
+ html[data-dsh-prts] [data-prts-session-lifeline-filament="core"] {
176
+ stroke: var(--prts-lifeline-green);
177
+ opacity: .68;
178
+ stroke-width: .9;
179
+ }
180
+
181
+ html[data-dsh-prts] [data-prts-session-lifeline-filament="front"] {
182
+ stroke: var(--prts-lifeline-blue);
183
+ opacity: .74;
184
+ stroke-width: .85;
185
+ }
186
+
187
+ html[data-dsh-prts] [data-prts-session-row][aria-selected="true"] [data-prts-session-lifeline] {
188
+ opacity: .62;
189
+ }
190
+
191
+ html[data-dsh-prts] [data-prts-session-row][aria-selected="true"]:hover [data-prts-session-lifeline] {
192
+ opacity: 0;
193
+ }
194
+
195
+ html[data-dsh-prts][data-prts-session-flow="off"] [data-prts-session-row] [data-prts-session-lifeline] {
196
+ opacity: 0;
197
+ }
198
+
199
+ @media (prefers-reduced-motion: reduce) {
200
+ html[data-dsh-prts] [data-prts-session-lifeline] {
201
+ transition-duration: 0s;
202
+ }
203
+ }
204
+
205
+ html[data-dsh-prts][data-prts-motion="reduced"] [data-prts-session-lifeline] {
206
+ transition-duration: 0s;
207
+ }
208
+
144
209
  html[data-dsh-prts] [data-prts-session-row]:hover [data-prts-facility-texture="pickup"] [data-prts-session-pickup] {
145
210
  opacity: .58;
146
211
  transform: translate3d(0, 0, 0);
@@ -809,7 +809,6 @@ html[data-dsh-prts] [data-prts-region="operation"] {
809
809
  html[data-dsh-prts] [data-prts-region="sessions"] {
810
810
  box-sizing: border-box;
811
811
  position: relative;
812
- z-index: 4;
813
812
  overflow: hidden;
814
813
  border-right: 1px solid var(--prts-line);
815
814
  background: var(--prts-panel);
@@ -818,7 +817,6 @@ html[data-dsh-prts] [data-prts-region="sessions"] {
818
817
 
819
818
  html[data-dsh-prts] [data-prts-region="operation"] {
820
819
  position: relative;
821
- z-index: 1;
822
820
  overflow: hidden;
823
821
  background: var(--prts-canvas);
824
822
  color: var(--prts-ink);
@@ -987,6 +985,14 @@ html[data-dsh-prts] [data-prts-session-row] [data-prts-row-title]::before {
987
985
  letter-spacing: .08em;
988
986
  }
989
987
 
988
+ html[data-dsh-prts] [data-prts-session-row][aria-selected="true"] [data-prts-row-title] {
989
+ font-weight: 700;
990
+ }
991
+
992
+ html[data-dsh-prts] [data-prts-session-row][aria-selected="true"] [data-prts-row-title]::before {
993
+ color: var(--prts-yellow);
994
+ }
995
+
990
996
  html[data-dsh-prts] [data-prts-workspace-actions],
991
997
  html[data-dsh-prts] [data-prts-session-actions] {
992
998
  position: absolute;
@@ -1516,7 +1522,7 @@ html[data-dsh-prts] [data-prts-composer-fallback] :is(textarea, [data-composer-i
1516
1522
  caret-color: var(--prts-yellow);
1517
1523
  }
1518
1524
 
1519
- html[data-dsh-prts] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *),
1525
+ html[data-dsh-prts] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *):not([data-prts-glass-menu] *):not([data-prts-floating-glass] *):not([role="menu"] *):not([role="listbox"] *),
1520
1526
  html[data-dsh-prts] [data-prts-conversation-control] {
1521
1527
  border: 1px solid var(--prts-line) !important;
1522
1528
  border-radius: var(--prts-radius-control) !important;
@@ -1526,7 +1532,7 @@ html[data-dsh-prts] [data-prts-conversation-control] {
1526
1532
  }
1527
1533
 
1528
1534
 
1529
- html[data-dsh-prts] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *):is(:hover, :focus-visible),
1535
+ html[data-dsh-prts] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *):not([data-prts-glass-menu] *):not([data-prts-floating-glass] *):not([role="menu"] *):not([role="listbox"] *):is(:hover, :focus-visible),
1530
1536
  html[data-dsh-prts] [data-prts-conversation-control]:is(:hover, :focus-visible) {
1531
1537
  border-color: var(--prts-yellow) !important;
1532
1538
  }
@@ -1537,7 +1543,7 @@ html[data-dsh-prts] [data-composer-card] button[type="submit"] {
1537
1543
  color: var(--prts-yellow-ink) !important;
1538
1544
  }
1539
1545
 
1540
- html[data-dsh-prts] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *):active,
1546
+ html[data-dsh-prts] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *):not([data-prts-glass-menu] *):not([data-prts-floating-glass] *):not([role="menu"] *):not([role="listbox"] *):active,
1541
1547
  html[data-dsh-prts] [data-prts-conversation-control]:active {
1542
1548
  transform: translateY(1px) scale(.98);
1543
1549
  }
@@ -11,7 +11,6 @@ html[data-dsh-prts] [data-prts-region="frame"] {
11
11
 
12
12
  html[data-dsh-prts] [data-prts-region="frame"] > [data-shell-overlay] {
13
13
  position: absolute !important;
14
- z-index: 120;
15
14
  inset: 0;
16
15
  min-width: 0;
17
16
  min-height: 0;
@@ -155,6 +154,10 @@ html[data-dsh-prts] [data-prts-session-row]:hover [data-prts-session-time] {
155
154
  opacity: 0;
156
155
  }
157
156
 
157
+ html[data-dsh-prts] [data-prts-session-row][aria-selected="true"] [data-prts-session-time] {
158
+ opacity: 0;
159
+ }
160
+
158
161
  /* Restore the notched P.R.T.S. composer with one complete layered contour. */
159
162
  html[data-dsh-prts] [data-composer-card],
160
163
  html[data-dsh-prts] [data-prts-composer-fallback] {
@@ -417,7 +420,7 @@ html[data-dsh-prts] [data-prts-hero-active] [data-prts-composer-fallback] textar
417
420
  resize: vertical;
418
421
  }
419
422
 
420
- html[data-dsh-prts] [data-prts-hero-active] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *),
423
+ html[data-dsh-prts] [data-prts-hero-active] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *):not([data-prts-glass-menu] *):not([data-prts-floating-glass] *):not([role="menu"] *):not([role="listbox"] *),
421
424
  html[data-dsh-prts] [data-prts-hero-active] [data-prts-glass-control] {
422
425
  min-height: 36px;
423
426
  }
@@ -137,7 +137,7 @@ html[data-dsh-prts] [data-prts-nav-bottom] b {
137
137
  /* Complete structural outlines; decorative accents are separate edges. */
138
138
  html[data-dsh-prts] [data-composer-card],
139
139
  html[data-dsh-prts] [data-prts-composer-fallback],
140
- html[data-dsh-prts] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *),
140
+ html[data-dsh-prts] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *):not([data-prts-glass-menu] *):not([data-prts-floating-glass] *):not([role="menu"] *):not([role="listbox"] *),
141
141
  html[data-dsh-prts] [data-prts-conversation-control],
142
142
  html[data-dsh-prts] [data-produced-files-row] > button[type="button"],
143
143
  html[data-dsh-prts] [data-prts-to-bottom] {