@ash-qw/dsh-theme-prts 0.1.98 → 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/CHANGELOG.md +12 -0
- package/lib/client.js +584 -14
- package/package.json +2 -2
- package/src/client/composer-glass-adapter.js +13 -3
- package/src/client/facility-vector.js +35 -0
- package/src/client/index.js +18 -2
- package/src/client/preferences.js +70 -4
- package/src/client/session-lifeline.js +270 -0
- package/src/client/sidebar-control-adapter.js +12 -0
- package/src/client/theme-controller-v2.js +15 -0
- package/src/client/theme-controller.js +15 -0
- package/src/client/theme-settings-workbench.js +199 -5
- package/src/styles/facility-vector.css +132 -0
- package/src/styles/prts.css +11 -5
- package/src/styles/refinement.css +5 -2
- package/src/styles/stabilization.css +1 -1
- package/src/styles/theme-settings-workbench.css +83 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ash-qw/dsh-theme-prts",
|
|
3
|
-
"version": "0.1.
|
|
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
|
|
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
|
}
|
|
@@ -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,22 @@ 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
|
+
const filament = svgNode(document, 'g', {
|
|
127
|
+
'data-prts-session-lifeline-filament': strand,
|
|
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)
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
const pickup = svgNode(document, 'g', {
|
|
121
138
|
'data-prts-session-pickup': '',
|
|
122
139
|
'clip-path': `url(#${clipId})`,
|
|
@@ -146,9 +163,22 @@ function appendSessionPickup(document, graphic) {
|
|
|
146
163
|
})
|
|
147
164
|
|
|
148
165
|
graphic.insertBefore(defs, graphic.firstChild)
|
|
166
|
+
graphic.insertBefore(lifeline, graphic.querySelector('[data-prts-facility-layer="outline"]'))
|
|
149
167
|
graphic.insertBefore(pickup, graphic.querySelector('[data-prts-facility-layer="outline"]'))
|
|
150
168
|
}
|
|
151
169
|
|
|
170
|
+
function clamp(value, min, max) {
|
|
171
|
+
return Math.min(max, Math.max(min, value))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sessionLifelineGeometry(width, height) {
|
|
175
|
+
const start = clamp(width * .04, 8, 12)
|
|
176
|
+
const end = Math.max(start + 1, width - 8)
|
|
177
|
+
const baseline = height * .56
|
|
178
|
+
const lift = clamp(height * .35, 10, 14)
|
|
179
|
+
return { start, end, baseline, lift }
|
|
180
|
+
}
|
|
181
|
+
|
|
152
182
|
export function ensureFacilityGraphic(document, owner, { kind, topNotch = false, texture = 'none' } = {}) {
|
|
153
183
|
if (!owner) return null
|
|
154
184
|
let graphic = owner.querySelector?.(':scope > svg[data-prts-facility-svg]')
|
|
@@ -200,6 +230,8 @@ export function renderFacilityGraphic(owner, { width, height } = {}) {
|
|
|
200
230
|
}
|
|
201
231
|
|
|
202
232
|
const size = `${Number(width.toFixed(3))}x${Number(height.toFixed(3))}`
|
|
233
|
+
const lifeline = graphic.querySelector('[data-prts-session-lifeline]')
|
|
234
|
+
const lifelineGeometry = lifeline ? sessionLifelineGeometry(width, height) : undefined
|
|
203
235
|
if (owner.getAttribute('data-prts-facility-size') !== size) {
|
|
204
236
|
graphic.setAttribute('viewBox', `0 0 ${Number(width.toFixed(3))} ${Number(height.toFixed(3))}`)
|
|
205
237
|
for (const layer of graphic.querySelectorAll('[data-prts-facility-layer], [data-prts-facility-clip]')) layer.setAttribute('d', path)
|
|
@@ -223,6 +255,9 @@ export function renderFacilityGraphic(owner, { width, height } = {}) {
|
|
|
223
255
|
bar.setAttribute('y', String(Number((center - barHeight / 2).toFixed(3))))
|
|
224
256
|
}
|
|
225
257
|
}
|
|
258
|
+
if (lifeline) {
|
|
259
|
+
configureSessionLifeline(lifeline, lifelineGeometry)
|
|
260
|
+
}
|
|
226
261
|
owner.setAttribute('data-prts-facility-size', size)
|
|
227
262
|
}
|
|
228
263
|
setReady(owner, true)
|
package/src/client/index.js
CHANGED
|
@@ -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
|
|
197
|
-
|
|
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:
|
|
20
|
+
version: 12,
|
|
10
21
|
enabled: false,
|
|
11
22
|
preset: 'standard-tactical',
|
|
12
23
|
texture: 'full',
|
|
@@ -18,6 +29,13 @@ export const DEFAULT_PREFERENCES = Object.freeze({
|
|
|
18
29
|
particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
|
|
19
30
|
particlePattern: 'orthogonal',
|
|
20
31
|
railDefaultHidden: false,
|
|
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,
|
|
21
39
|
conversationStyle: 'native',
|
|
22
40
|
})
|
|
23
41
|
|
|
@@ -60,7 +78,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
|
|
|
60
78
|
material: Object.freeze(['glass']),
|
|
61
79
|
accessibility: Object.freeze(['motion', 'bootAnimation']),
|
|
62
80
|
navigation: Object.freeze(['railDefaultHidden']),
|
|
63
|
-
conversation: Object.freeze(['conversationStyle']),
|
|
81
|
+
conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle']),
|
|
64
82
|
})
|
|
65
83
|
|
|
66
84
|
const TEXTURES = new Set(['off', 'restrained', 'full'])
|
|
@@ -69,6 +87,8 @@ const MOTIONS = new Set(['system', 'reduced'])
|
|
|
69
87
|
const PARTICLE_DENSITIES = new Set(['sparse', 'light', 'standard', 'dense', 'ultra'])
|
|
70
88
|
const PRESETS = new Set([...Object.keys(VISUAL_PRESETS), 'custom'])
|
|
71
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'])
|
|
72
92
|
const PRESET_LINKED_KEYS = new Set(Object.keys(VISUAL_PRESETS['standard-tactical']))
|
|
73
93
|
|
|
74
94
|
|
|
@@ -98,6 +118,10 @@ function enumOr(value, allowed, fallback) {
|
|
|
98
118
|
return allowed.has(value) ? value : fallback
|
|
99
119
|
}
|
|
100
120
|
|
|
121
|
+
function colorOr(value, fallback) {
|
|
122
|
+
return typeof value === 'string' && /^#[\da-f]{6}$/i.test(value) ? value.toLowerCase() : fallback
|
|
123
|
+
}
|
|
124
|
+
|
|
101
125
|
function particleTraversalSpeedOr(value, fallback) {
|
|
102
126
|
const numeric = Number(value)
|
|
103
127
|
if (!Number.isFinite(numeric)) return fallback
|
|
@@ -105,6 +129,13 @@ function particleTraversalSpeedOr(value, fallback) {
|
|
|
105
129
|
return Math.round(clamped / PARTICLE_TRAVERSAL_SPEED_STEP) * PARTICLE_TRAVERSAL_SPEED_STEP
|
|
106
130
|
}
|
|
107
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
|
+
|
|
108
139
|
function normalizeGlass(input) {
|
|
109
140
|
if (input.glassEnabled === false || input.glass === 'off') return 'off'
|
|
110
141
|
if (input.glass === 'liquid') return 'clear'
|
|
@@ -131,8 +162,16 @@ export function normalizePreferences(value) {
|
|
|
131
162
|
: input.preset
|
|
132
163
|
const particlePreset = VISUAL_PRESETS[input.preset] ?? DEFAULT_PREFERENCES
|
|
133
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
|
|
134
173
|
const normalized = {
|
|
135
|
-
version:
|
|
174
|
+
version: 12,
|
|
136
175
|
enabled: booleanOr(input.enabled, DEFAULT_PREFERENCES.enabled),
|
|
137
176
|
preset: enumOr(migratedPreset, PRESETS, DEFAULT_PREFERENCES.preset),
|
|
138
177
|
texture: enumOr(input.texture, TEXTURES, DEFAULT_PREFERENCES.texture),
|
|
@@ -147,6 +186,13 @@ export function normalizePreferences(value) {
|
|
|
147
186
|
),
|
|
148
187
|
particlePattern: 'orthogonal',
|
|
149
188
|
railDefaultHidden: booleanOr(input.railDefaultHidden, DEFAULT_PREFERENCES.railDefaultHidden),
|
|
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),
|
|
150
196
|
conversationStyle: enumOr(input.conversationStyle, CONVERSATION_STYLES, DEFAULT_PREFERENCES.conversationStyle),
|
|
151
197
|
}
|
|
152
198
|
normalized.preset = matchedVisualPreset(normalized)
|
|
@@ -173,6 +219,26 @@ export function updatePreferenceValue(value, key, nextValue) {
|
|
|
173
219
|
if (key === 'preset') return applyVisualPreset(value, nextValue)
|
|
174
220
|
if (key === 'particleDetail') return applyParticleDetail(value, nextValue)
|
|
175
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
|
+
}
|
|
176
242
|
if (key === 'conversationParticleDensity' || key === 'heroParticleDensity') {
|
|
177
243
|
const density = enumOr(nextValue, PARTICLE_DENSITIES, current[key])
|
|
178
244
|
return normalizePreferences({ ...current, [key]: density })
|
|
@@ -194,7 +260,7 @@ export function loadPreferences(storage) {
|
|
|
194
260
|
const raw = storage?.getItem(PRTS_STORAGE_KEY)
|
|
195
261
|
if (raw === null || raw === undefined) return { ...DEFAULT_PREFERENCES }
|
|
196
262
|
const parsed = JSON.parse(raw)
|
|
197
|
-
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 }
|
|
198
264
|
return normalizePreferences(parsed)
|
|
199
265
|
} catch {
|
|
200
266
|
return { ...DEFAULT_PREFERENCES }
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
const HOVER_SETTLE = 180
|
|
2
|
+
const STATIC_TIME = 2.4
|
|
3
|
+
const POINT_COUNT = 9
|
|
4
|
+
const DEFAULT_FLOW_SPEED = 1
|
|
5
|
+
|
|
6
|
+
function clamp(value, min, max) {
|
|
7
|
+
return Math.min(max, Math.max(min, value))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function number(value) {
|
|
11
|
+
return Number(value.toFixed(3))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readGeometry(lifeline) {
|
|
15
|
+
const start = Number(lifeline?.getAttribute?.('data-prts-lifeline-start'))
|
|
16
|
+
const end = Number(lifeline?.getAttribute?.('data-prts-lifeline-end'))
|
|
17
|
+
const baseline = Number(lifeline?.getAttribute?.('data-prts-lifeline-baseline'))
|
|
18
|
+
const lift = Number(lifeline?.getAttribute?.('data-prts-lifeline-lift'))
|
|
19
|
+
if (![start, end, baseline, lift].every(Number.isFinite) || end <= start || lift <= 0) return null
|
|
20
|
+
return { start, end, baseline, lift }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function filamentNodes(lifeline) {
|
|
24
|
+
if (!lifeline) return []
|
|
25
|
+
if (lifeline.matches?.('[data-prts-session-lifeline-filament]')) return [lifeline]
|
|
26
|
+
return Array.from(lifeline.querySelectorAll?.('[data-prts-session-lifeline-filament]') ?? [])
|
|
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
|
+
|
|
38
|
+
function createSmoothPath(points) {
|
|
39
|
+
if (points.length < 2) return ''
|
|
40
|
+
const commands = [`M ${number(points[0].x)} ${number(points[0].y)}`]
|
|
41
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
42
|
+
const previous = points[Math.max(0, index - 1)]
|
|
43
|
+
const current = points[index]
|
|
44
|
+
const next = points[index + 1]
|
|
45
|
+
const following = points[Math.min(points.length - 1, index + 2)]
|
|
46
|
+
const firstControl = {
|
|
47
|
+
x: current.x + (next.x - previous.x) / 6,
|
|
48
|
+
y: current.y + (next.y - previous.y) / 6,
|
|
49
|
+
}
|
|
50
|
+
const secondControl = {
|
|
51
|
+
x: next.x - (following.x - current.x) / 6,
|
|
52
|
+
y: next.y - (following.y - current.y) / 6,
|
|
53
|
+
}
|
|
54
|
+
commands.push([
|
|
55
|
+
'C',
|
|
56
|
+
number(firstControl.x),
|
|
57
|
+
number(firstControl.y),
|
|
58
|
+
number(secondControl.x),
|
|
59
|
+
number(secondControl.y),
|
|
60
|
+
number(next.x),
|
|
61
|
+
number(next.y),
|
|
62
|
+
].join(' '))
|
|
63
|
+
}
|
|
64
|
+
return commands.join(' ')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sampleFlow(u, time, strand) {
|
|
68
|
+
const edgeEnvelope = Math.sin(Math.PI * u) ** .88
|
|
69
|
+
const slowTurn = Math.sin(time * .43 + u * Math.PI * 1.16 + Math.sin(time * .13) * .48)
|
|
70
|
+
const crossCurrent = Math.sin(time * -.29 + u * Math.PI * 2.42 + .7)
|
|
71
|
+
const fineCurrent = Math.sin(time * .19 + u * Math.PI * 3.76 - .9)
|
|
72
|
+
const sharedFlow = slowTurn * .52 + crossCurrent * .24 + fineCurrent * .08
|
|
73
|
+
|
|
74
|
+
const fan = Math.sin(Math.PI * u) ** 1.35
|
|
75
|
+
const separation = strand * (.15 + Math.sin(time * .31 + u * Math.PI * 1.48 + strand * .8) * .1)
|
|
76
|
+
const independentDrift = Math.sin(time * (.36 + strand * .021) + u * Math.PI * 2.18 + strand * 1.35) * .055
|
|
77
|
+
return edgeEnvelope * clamp(sharedFlow + fan * separation + independentDrift, -.9, .9)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createSessionLifelinePath(geometry, time = STATIC_TIME, strength = 1, strand = 0) {
|
|
81
|
+
const { start, end, baseline, lift } = geometry
|
|
82
|
+
const span = end - start
|
|
83
|
+
const tension = clamp(strength, 0, 1)
|
|
84
|
+
const points = []
|
|
85
|
+
for (let index = 0; index < POINT_COUNT; index += 1) {
|
|
86
|
+
const u = index / (POINT_COUNT - 1)
|
|
87
|
+
points.push({
|
|
88
|
+
x: start + span * u,
|
|
89
|
+
y: baseline - lift * sampleFlow(u, time, strand) * tension,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
return createSmoothPath(points)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function configureSessionLifeline(lifeline, { start, end, baseline, lift }) {
|
|
96
|
+
if (!lifeline) return false
|
|
97
|
+
const geometry = { start, end, baseline, lift }
|
|
98
|
+
lifeline.setAttribute('data-prts-lifeline-start', String(number(start)))
|
|
99
|
+
lifeline.setAttribute('data-prts-lifeline-end', String(number(end)))
|
|
100
|
+
lifeline.setAttribute('data-prts-lifeline-baseline', String(number(baseline)))
|
|
101
|
+
lifeline.setAttribute('data-prts-lifeline-lift', String(number(lift)))
|
|
102
|
+
filamentNodes(lifeline).forEach((filament, index, filaments) => {
|
|
103
|
+
const strand = filaments.length === 1 ? 0 : index - (filaments.length - 1) / 2
|
|
104
|
+
setFilamentPath(filament, createSessionLifelinePath(geometry, STATIC_TIME, 1, strand))
|
|
105
|
+
})
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createSessionLifelineAnimator({ document, window }) {
|
|
110
|
+
const requestFrame = window?.requestAnimationFrame?.bind(window)
|
|
111
|
+
?? (callback => window?.setTimeout?.(() => callback(Date.now()), 1000 / 60))
|
|
112
|
+
const cancelFrame = window?.cancelAnimationFrame?.bind(window)
|
|
113
|
+
?? window?.clearTimeout?.bind(window)
|
|
114
|
+
const reducedMotion = window?.matchMedia?.('(prefers-reduced-motion: reduce)')
|
|
115
|
+
const MotionObserver = window?.MutationObserver
|
|
116
|
+
|
|
117
|
+
let frame
|
|
118
|
+
let lastTime
|
|
119
|
+
let flowTime = STATIC_TIME
|
|
120
|
+
let strength = 1
|
|
121
|
+
let row
|
|
122
|
+
let lifeline
|
|
123
|
+
let filaments = []
|
|
124
|
+
let hovered = false
|
|
125
|
+
let motionObserver
|
|
126
|
+
|
|
127
|
+
function render() {
|
|
128
|
+
const geometry = readGeometry(lifeline)
|
|
129
|
+
if (!geometry || filaments.length === 0) return false
|
|
130
|
+
filaments.forEach((filament, index) => {
|
|
131
|
+
const strand = filaments.length === 1 ? 0 : index - (filaments.length - 1) / 2
|
|
132
|
+
setFilamentPath(filament, createSessionLifelinePath(geometry, flowTime, strength, strand))
|
|
133
|
+
})
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function canAnimate() {
|
|
138
|
+
return Boolean(
|
|
139
|
+
row?.isConnected
|
|
140
|
+
&& lifeline?.isConnected
|
|
141
|
+
&& row.getAttribute('aria-selected') === 'true'
|
|
142
|
+
&& document?.visibilityState !== 'hidden'
|
|
143
|
+
&& document?.documentElement?.dataset?.prtsSessionFlow !== 'off'
|
|
144
|
+
&& document?.documentElement?.dataset?.prtsMotion !== 'reduced'
|
|
145
|
+
&& !reducedMotion?.matches
|
|
146
|
+
)
|
|
147
|
+
}
|
|
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
|
+
|
|
154
|
+
function cancel() {
|
|
155
|
+
if (frame !== undefined) cancelFrame?.(frame)
|
|
156
|
+
frame = undefined
|
|
157
|
+
lastTime = undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function needsFrame() {
|
|
161
|
+
return canAnimate() && (!hovered || strength > 0)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function schedule() {
|
|
165
|
+
if (frame !== undefined || !needsFrame()) return
|
|
166
|
+
frame = requestFrame?.(tick)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function tick(time) {
|
|
170
|
+
frame = undefined
|
|
171
|
+
if (!canAnimate()) {
|
|
172
|
+
lastTime = undefined
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
const now = Number.isFinite(time) ? time : Date.now()
|
|
176
|
+
if (lastTime === undefined) lastTime = now
|
|
177
|
+
const elapsed = Math.min(100, Math.max(0, now - lastTime))
|
|
178
|
+
lastTime = now
|
|
179
|
+
flowTime += elapsed / 1000 * currentFlowSpeed()
|
|
180
|
+
const target = hovered ? 0 : 1
|
|
181
|
+
if (strength !== target) {
|
|
182
|
+
const step = elapsed / HOVER_SETTLE
|
|
183
|
+
strength = target > strength
|
|
184
|
+
? Math.min(target, strength + step)
|
|
185
|
+
: Math.max(target, strength - step)
|
|
186
|
+
}
|
|
187
|
+
render()
|
|
188
|
+
schedule()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function motionReduced() {
|
|
192
|
+
return document?.documentElement?.dataset?.prtsMotion === 'reduced' || reducedMotion?.matches
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function syncMotion() {
|
|
196
|
+
if (canAnimate()) {
|
|
197
|
+
schedule()
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
cancel()
|
|
201
|
+
if (lifeline && motionReduced()) {
|
|
202
|
+
flowTime = STATIC_TIME
|
|
203
|
+
strength = 1
|
|
204
|
+
render()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function onPointerEnter() {
|
|
209
|
+
hovered = true
|
|
210
|
+
syncMotion()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function onPointerLeave() {
|
|
214
|
+
hovered = false
|
|
215
|
+
syncMotion()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function detachRow() {
|
|
219
|
+
row?.removeEventListener?.('pointerenter', onPointerEnter)
|
|
220
|
+
row?.removeEventListener?.('pointerleave', onPointerLeave)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function attachObservers() {
|
|
224
|
+
document?.addEventListener?.('visibilitychange', syncMotion)
|
|
225
|
+
reducedMotion?.addEventListener?.('change', syncMotion)
|
|
226
|
+
if (!motionObserver && typeof MotionObserver === 'function' && document?.documentElement) {
|
|
227
|
+
motionObserver = new MotionObserver(syncMotion)
|
|
228
|
+
motionObserver.observe(document.documentElement, {
|
|
229
|
+
attributes: true,
|
|
230
|
+
attributeFilter: ['data-prts-motion', 'data-prts-session-flow', 'data-prts-session-flow-speed'],
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
start() {
|
|
237
|
+
attachObservers()
|
|
238
|
+
syncMotion()
|
|
239
|
+
},
|
|
240
|
+
setTarget(nextRow, nextLifeline) {
|
|
241
|
+
if (row === nextRow && lifeline === nextLifeline) {
|
|
242
|
+
syncMotion()
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
cancel()
|
|
246
|
+
detachRow()
|
|
247
|
+
row = nextRow
|
|
248
|
+
lifeline = nextLifeline
|
|
249
|
+
filaments = filamentNodes(lifeline)
|
|
250
|
+
hovered = Boolean(row?.matches?.(':hover'))
|
|
251
|
+
flowTime = STATIC_TIME
|
|
252
|
+
strength = hovered ? 0 : 1
|
|
253
|
+
row?.addEventListener?.('pointerenter', onPointerEnter)
|
|
254
|
+
row?.addEventListener?.('pointerleave', onPointerLeave)
|
|
255
|
+
render()
|
|
256
|
+
syncMotion()
|
|
257
|
+
},
|
|
258
|
+
dispose() {
|
|
259
|
+
cancel()
|
|
260
|
+
detachRow()
|
|
261
|
+
row = undefined
|
|
262
|
+
lifeline = undefined
|
|
263
|
+
filaments = []
|
|
264
|
+
motionObserver?.disconnect()
|
|
265
|
+
motionObserver = undefined
|
|
266
|
+
document?.removeEventListener?.('visibilitychange', syncMotion)
|
|
267
|
+
reducedMotion?.removeEventListener?.('change', syncMotion)
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -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,8 +7,17 @@ const ROOT_ATTRIBUTES = [
|
|
|
7
7
|
'data-prts-glass',
|
|
8
8
|
'data-prts-motion',
|
|
9
9
|
'data-prts-particle-pattern',
|
|
10
|
+
'data-prts-session-flow',
|
|
11
|
+
'data-prts-session-glow',
|
|
12
|
+
'data-prts-session-flow-palette',
|
|
13
|
+
'data-prts-session-flow-speed',
|
|
10
14
|
'data-prts-conversation-style',
|
|
11
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
|
+
}
|
|
12
21
|
|
|
13
22
|
const TRANSITION_ATTRIBUTE = 'data-prts-scheme-transition'
|
|
14
23
|
const TRANSITION_MODE_ATTRIBUTE = 'data-prts-scheme-transition-mode'
|
|
@@ -101,6 +110,7 @@ export function createThemeController({ document, window, cssText, service, onTr
|
|
|
101
110
|
|
|
102
111
|
function clearThemeState() {
|
|
103
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)
|
|
104
114
|
clearTransitionState()
|
|
105
115
|
}
|
|
106
116
|
|
|
@@ -716,6 +726,11 @@ export function createThemeController({ document, window, cssText, service, onTr
|
|
|
716
726
|
root.dataset.prtsGlass = preferences.glass
|
|
717
727
|
root.dataset.prtsMotion = preferences.motion
|
|
718
728
|
root.dataset.prtsParticlePattern = 'orthogonal'
|
|
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])
|
|
719
734
|
root.dataset.prtsConversationStyle = preferences.conversationStyle
|
|
720
735
|
refresh()
|
|
721
736
|
},
|