@ash-qw/dsh-theme-prts 1.4.1 → 1.4.2
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 +11 -0
- package/lib/client.js +469 -80
- package/package.json +1 -1
- package/src/client/index.js +1 -2
- package/src/client/session-lifeline.js +1 -1
- package/src/client/session-summary-preview.js +116 -2
- package/src/client/sidebar-control-adapter.js +365 -43
- package/src/client/theme-settings-workbench.js +15 -48
- package/src/styles/facility-vector.css +105 -21
- package/src/styles/prts.css +5 -5
- package/src/styles/refinement.css +1 -1
- package/src/styles/theme-settings-workbench.css +6 -32
|
@@ -20,6 +20,7 @@ const OWNED_ATTRIBUTES = [
|
|
|
20
20
|
'data-prts-session-time',
|
|
21
21
|
'data-prts-session-index',
|
|
22
22
|
'data-prts-session-state',
|
|
23
|
+
'data-prts-selection-target',
|
|
23
24
|
'data-prts-session-summary-header',
|
|
24
25
|
'data-prts-session-summary-label',
|
|
25
26
|
'data-prts-facility-face',
|
|
@@ -190,8 +191,23 @@ function ensureFacilitySpine(document, row, kind, next) {
|
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
|
|
193
|
-
|
|
194
|
+
function createRunningMarker(document) {
|
|
195
|
+
const svgNode = name => document.createElementNS('http://www.w3.org/2000/svg', name)
|
|
196
|
+
const marker = svgNode('g')
|
|
197
|
+
marker.setAttribute('data-prts-running-marker', '')
|
|
198
|
+
const letter = svgNode('text')
|
|
199
|
+
letter.setAttribute('x', '12')
|
|
200
|
+
letter.setAttribute('y', '16')
|
|
201
|
+
letter.textContent = 'A'
|
|
202
|
+
const ring = svgNode('circle')
|
|
203
|
+
for (const [key, value] of Object.entries({cx:12, cy:12, r:10, 'stroke-dasharray':'4.5 11.208'})) ring.setAttribute(key, value)
|
|
204
|
+
marker.append(letter, ring)
|
|
205
|
+
return marker
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createSidebarControlAdapter({ document, window, sessions, workspaces, getSessionStatus, getUiWorkspace, getUiConversation, getSessions, getWorkspaces }) {
|
|
194
209
|
let previewPreferences = {}
|
|
210
|
+
let adapterActive = false, sourceRetryTimer
|
|
195
211
|
let observer
|
|
196
212
|
let resizeObserver
|
|
197
213
|
let resizeFrame
|
|
@@ -204,6 +220,46 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
204
220
|
let summaryRoot
|
|
205
221
|
let summarySignature = ''
|
|
206
222
|
let heldPreviewState
|
|
223
|
+
let hoveredSummary, summaryLeaveTimer
|
|
224
|
+
let summaryDismissed = false, pointerLeftSummary = false
|
|
225
|
+
function resumeSummaryHover(event) {
|
|
226
|
+
if (!summaryDismissed) return
|
|
227
|
+
const trigger = event.target.closest?.('[data-prts-session-summary-trigger]')
|
|
228
|
+
if (!trigger || !summaryRoot?.contains(trigger)) { pointerLeftSummary = true; return }
|
|
229
|
+
if (!pointerLeftSummary) return
|
|
230
|
+
summaryDismissed = false
|
|
231
|
+
summaryRoot.removeAttribute('data-prts-summary-dismissed')
|
|
232
|
+
document.removeEventListener('pointermove', resumeSummaryHover)
|
|
233
|
+
keepSummaryHover(trigger.closest('[data-prts-session-summary-group]'))
|
|
234
|
+
}
|
|
235
|
+
function closeNavigationPreview() {
|
|
236
|
+
heldPreviewState = undefined
|
|
237
|
+
preview.stop()
|
|
238
|
+
clearSummaryHover()
|
|
239
|
+
summaryDismissed = true
|
|
240
|
+
pointerLeftSummary = false
|
|
241
|
+
summaryRoot?.setAttribute('data-prts-summary-dismissed', '')
|
|
242
|
+
for (const group of summaryRoot?.querySelectorAll('[data-prts-session-summary-group]') ?? []) group.removeAttribute('data-prts-preview-open')
|
|
243
|
+
document.addEventListener('pointermove', resumeSummaryHover)
|
|
244
|
+
}
|
|
245
|
+
function clearSummaryHover() {
|
|
246
|
+
window.clearTimeout(summaryLeaveTimer)
|
|
247
|
+
hoveredSummary?.removeAttribute('data-prts-summary-hover-open')
|
|
248
|
+
hoveredSummary?.removeAttribute('data-prts-summary-instant')
|
|
249
|
+
hoveredSummary = undefined
|
|
250
|
+
}
|
|
251
|
+
function keepSummaryHover(group) {
|
|
252
|
+
if (summaryDismissed) return
|
|
253
|
+
window.clearTimeout(summaryLeaveTimer)
|
|
254
|
+
if (!hoveredSummary) group.removeAttribute('data-prts-summary-instant')
|
|
255
|
+
if (hoveredSummary && hoveredSummary !== group) {
|
|
256
|
+
hoveredSummary.setAttribute('data-prts-summary-instant', '')
|
|
257
|
+
hoveredSummary.removeAttribute('data-prts-summary-hover-open')
|
|
258
|
+
group.setAttribute('data-prts-summary-instant', '')
|
|
259
|
+
}
|
|
260
|
+
hoveredSummary = group
|
|
261
|
+
group.setAttribute('data-prts-summary-hover-open', '')
|
|
262
|
+
}
|
|
207
263
|
function dismissHeldPreview(event) {
|
|
208
264
|
if (!heldPreviewState || (event?.type === 'keydown' && event.key !== 'Escape')) return
|
|
209
265
|
if (event?.type === 'pointerdown' && (summaryRoot?.contains(event.target) || event.target.closest?.('[data-prts-summary-reply]'))) return
|
|
@@ -217,13 +273,70 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
217
273
|
const preview = createSessionSummaryPreview({ document, window, sessions, getUiConversation, getSessions, getSessionStatus, openSession, getPreviewPreferences: () => previewPreferences, onPreviewClosed: () => { if (statusSourcesStarted) queueScan() }, onAnswerSubmitted: state => { heldPreviewState = state; queueScan() } })
|
|
218
274
|
const stopTitles = new Map()
|
|
219
275
|
const summaryGroups = new Map()
|
|
220
|
-
function clearSummaryEffects() { preview.stop(false); for (const stop of stopTitles.values()) stop(); stopTitles.clear(); summaryGroups.clear() }
|
|
276
|
+
function clearSummaryEffects() { clearSummaryHover(); preview.stop(false); for (const entry of summaryGroups.values()) entry.dispose(); for (const stop of stopTitles.values()) stop(); stopTitles.clear(); summaryGroups.clear() }
|
|
221
277
|
let owned = new Set()
|
|
222
278
|
let vectorOwners = new Set()
|
|
223
279
|
const dirtyVectors = new Map()
|
|
224
280
|
const vectorSizes = new WeakMap()
|
|
225
281
|
const lifelineAnimator = createSessionLifelineAnimator({ document, window })
|
|
226
282
|
|
|
283
|
+
let subscribedSessions, subscribedWorkspaces
|
|
284
|
+
function disconnectSummarySources() {
|
|
285
|
+
statusSourcesStarted = false
|
|
286
|
+
window.clearTimeout(sourceRetryTimer)
|
|
287
|
+
sourceRetryTimer = undefined
|
|
288
|
+
stopSessionList()
|
|
289
|
+
stopWorkspaceList()
|
|
290
|
+
stopSessionStatus()
|
|
291
|
+
stopSessionList = stopWorkspaceList = stopSessionStatus = () => {}
|
|
292
|
+
subscribedSessions = subscribedWorkspaces = sessionStatus = undefined
|
|
293
|
+
if (getSessions) sessions = undefined
|
|
294
|
+
if (getWorkspaces) workspaces = undefined
|
|
295
|
+
cancelReveal()
|
|
296
|
+
cancelPendingNavigation()
|
|
297
|
+
clearSelectionTarget()
|
|
298
|
+
heldPreviewState = undefined
|
|
299
|
+
summaryDismissed = false
|
|
300
|
+
document?.removeEventListener('pointermove', resumeSummaryHover)
|
|
301
|
+
document?.removeEventListener('pointerdown', dismissHeldPreview)
|
|
302
|
+
document?.removeEventListener('keydown', dismissHeldPreview)
|
|
303
|
+
preview.stop()
|
|
304
|
+
}
|
|
305
|
+
function syncSummarySources() {
|
|
306
|
+
if (previewPreferences.sessionPreview === false) {
|
|
307
|
+
if (statusSourcesStarted) disconnectSummarySources()
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
if (!statusSourcesStarted) {
|
|
311
|
+
statusSourcesStarted = true
|
|
312
|
+
document.addEventListener('pointerdown', dismissHeldPreview)
|
|
313
|
+
document.addEventListener('keydown', dismissHeldPreview)
|
|
314
|
+
}
|
|
315
|
+
connectSummarySources()
|
|
316
|
+
connectSessionStatus()
|
|
317
|
+
const waiting = (getSessions && !subscribedSessions) || (getWorkspaces && !subscribedWorkspaces)
|
|
318
|
+
|| (getSessionStatus && !sessionStatus)
|
|
319
|
+
if (waiting && sourceRetryTimer === undefined) {
|
|
320
|
+
sourceRetryTimer = window.setTimeout(() => { sourceRetryTimer = undefined; queueScan() }, 250)
|
|
321
|
+
} else if (!waiting) {
|
|
322
|
+
window.clearTimeout(sourceRetryTimer)
|
|
323
|
+
sourceRetryTimer = undefined
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function connectSummarySources() {
|
|
327
|
+
if (getSessions) sessions = getSessions()
|
|
328
|
+
if (getWorkspaces) workspaces = getWorkspaces()
|
|
329
|
+
if (subscribedSessions !== sessions?.list) {
|
|
330
|
+
stopSessionList()
|
|
331
|
+
subscribedSessions = sessions?.list
|
|
332
|
+
stopSessionList = subscribedSessions?.subscribe?.(queueScan) ?? (() => {})
|
|
333
|
+
}
|
|
334
|
+
if (subscribedWorkspaces !== workspaces?.list) {
|
|
335
|
+
stopWorkspaceList()
|
|
336
|
+
subscribedWorkspaces = workspaces?.list
|
|
337
|
+
stopWorkspaceList = subscribedWorkspaces?.subscribe?.(queueScan) ?? (() => {})
|
|
338
|
+
}
|
|
339
|
+
}
|
|
227
340
|
function connectSessionStatus() {
|
|
228
341
|
const source = getSessionStatus?.()
|
|
229
342
|
if (source === sessionStatus) return
|
|
@@ -232,29 +345,139 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
232
345
|
stopSessionStatus = source?.subscribe?.(queueScan) ?? (() => {})
|
|
233
346
|
}
|
|
234
347
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
348
|
+
let selectionTarget, selectionTargetTimer
|
|
349
|
+
function clearSelectionTarget() {
|
|
350
|
+
selectionTarget = undefined
|
|
351
|
+
window.clearTimeout(selectionTargetTimer)
|
|
352
|
+
document?.removeEventListener('pointerdown', interruptSelectionTarget, true)
|
|
353
|
+
queueScan()
|
|
354
|
+
}
|
|
355
|
+
function interruptSelectionTarget(event) {
|
|
356
|
+
if (event.target.closest?.('[data-prts-session-row]')) clearSelectionTarget()
|
|
357
|
+
}
|
|
358
|
+
function setSelectionTarget(id) {
|
|
359
|
+
clearSelectionTarget()
|
|
360
|
+
selectionTarget = id
|
|
361
|
+
selectionTargetTimer = window.setTimeout(clearSelectionTarget, 4000)
|
|
362
|
+
document.addEventListener('pointerdown', interruptSelectionTarget, true)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let pendingReveal, revealTimer, revealFrame, revealSettleTimer
|
|
366
|
+
let navigationFrame, navigationGeneration = 0
|
|
367
|
+
function cancelPendingNavigation() {
|
|
368
|
+
navigationGeneration++
|
|
369
|
+
window.cancelAnimationFrame?.(navigationFrame)
|
|
370
|
+
}
|
|
371
|
+
function cancelReveal() {
|
|
372
|
+
pendingReveal = undefined
|
|
373
|
+
window.clearTimeout(revealTimer)
|
|
374
|
+
window.clearTimeout(revealSettleTimer)
|
|
375
|
+
window.cancelAnimationFrame?.(revealFrame)
|
|
376
|
+
}
|
|
377
|
+
function revealSessionCard(sessionId) {
|
|
378
|
+
cancelReveal()
|
|
379
|
+
pendingReveal = {sessionId, attempts: 0}
|
|
380
|
+
revealTimer = window.setTimeout(cancelReveal, 4000)
|
|
381
|
+
const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
|
|
382
|
+
syncSessionReveal(root)
|
|
383
|
+
queueScan()
|
|
384
|
+
return Boolean(pendingReveal?.expanding)
|
|
385
|
+
}
|
|
386
|
+
function syncSessionReveal(root) {
|
|
387
|
+
if (!pendingReveal || !root) return
|
|
388
|
+
const {sessionId} = pendingReveal
|
|
389
|
+
const rows = Array.from(root.querySelectorAll('[data-row-key], [data-session-id]'))
|
|
390
|
+
const row = rows.find(el => el.getAttribute('data-row-key') === `session:${sessionId}`
|
|
391
|
+
|| el.getAttribute('data-session-id') === sessionId)
|
|
392
|
+
if (row) {
|
|
393
|
+
if (pendingReveal.scrolling) return
|
|
394
|
+
const request = pendingReveal
|
|
395
|
+
request.scrolling = true
|
|
396
|
+
// Let React finish mounting and let the host's row glide settle before scrolling.
|
|
397
|
+
revealFrame = window.requestAnimationFrame(async () => {
|
|
398
|
+
const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
399
|
+
const animations = []
|
|
400
|
+
if (!reduced) for (let node = row; node && node !== root; node = node.parentElement) {
|
|
401
|
+
for (const animation of node.getAnimations?.() ?? []) {
|
|
402
|
+
const timing = animation.effect?.getComputedTiming?.()
|
|
403
|
+
if (animation.playState === 'running' && Number.isFinite(timing?.endTime)) animations.push(animation.finished)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (animations.length) await Promise.race([
|
|
407
|
+
Promise.allSettled(animations),
|
|
408
|
+
new Promise(resolve => { revealSettleTimer = window.setTimeout(resolve, 300) }),
|
|
409
|
+
])
|
|
410
|
+
if (pendingReveal !== request || !row.isConnected) return
|
|
411
|
+
row.scrollIntoView?.({block:'nearest', inline:'nearest', behavior:reduced ? 'instant' : 'smooth'})
|
|
412
|
+
cancelReveal()
|
|
413
|
+
})
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
if (++pendingReveal.attempts > 40) { cancelReveal(); return }
|
|
417
|
+
const items = snapshotOf(workspaces?.list)?.items ?? []
|
|
418
|
+
const owner = items.find(item => item.sessionIds?.includes(sessionId))
|
|
419
|
+
const key = owner?.workspaceId ?? ''
|
|
420
|
+
const keyed = key => rows.find(el => el.getAttribute('data-row-key') === key)
|
|
421
|
+
const group = keyed(`workspace:${key}`)
|
|
422
|
+
if (group?.getAttribute('aria-expanded') === 'false') { pendingReveal.expanding = true; group.click(); return }
|
|
423
|
+
if (!group && owner?.path) {
|
|
424
|
+
// Nested workspaces may not mount until their visible ancestor is expanded.
|
|
425
|
+
const path = value => value?.replace(/\\/g, '/').replace(/\/$/, '')
|
|
426
|
+
const target = path(owner.path)
|
|
427
|
+
const ancestors = items.filter(item => item !== owner && item.path && target.startsWith(path(item.path) + '/'))
|
|
428
|
+
.sort((a,b) => b.path.length - a.path.length)
|
|
429
|
+
for (const ancestor of ancestors) {
|
|
430
|
+
const parent = keyed(`workspace:${ancestor.workspaceId}`)
|
|
431
|
+
if (parent?.getAttribute('aria-expanded') === 'false') { pendingReveal.expanding = true; parent.click(); return }
|
|
246
432
|
}
|
|
247
|
-
}
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
433
|
+
}
|
|
434
|
+
const overflow = keyed(`overflow:${key}`)
|
|
435
|
+
if (overflow?.getAttribute('aria-expanded') === 'false') { pendingReveal.expanding = true; overflow.click() }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function openSession(sessionId) {
|
|
439
|
+
closeNavigationPreview()
|
|
440
|
+
cancelPendingNavigation()
|
|
441
|
+
setSelectionTarget(sessionId)
|
|
442
|
+
revealSessionCard(sessionId)
|
|
443
|
+
const generation = navigationGeneration
|
|
444
|
+
const navigate = () => {
|
|
445
|
+
if (generation !== navigationGeneration || !adapterActive) return
|
|
446
|
+
try {
|
|
447
|
+
const uiWorkspace = getUiWorkspace?.()
|
|
448
|
+
if (typeof uiWorkspace?.openSession === 'function') {
|
|
449
|
+
uiWorkspace.openSession(sessionId)
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
if (typeof sessions?.open === 'function') {
|
|
453
|
+
sessions.open(sessionId)
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
} catch {}
|
|
457
|
+
const row = Array.from(document.querySelectorAll('[data-prts-session-row]')).find(candidate => (
|
|
458
|
+
candidate.getAttribute('data-row-key') === `session:${sessionId}`
|
|
459
|
+
|| candidate.getAttribute('data-session-id') === sessionId
|
|
460
|
+
|| candidate.querySelector('[data-prts-row-title]')?.textContent.trim() === sessionId
|
|
461
|
+
))
|
|
462
|
+
if (row) row.click?.()
|
|
463
|
+
else clearSelectionTarget()
|
|
464
|
+
}
|
|
465
|
+
// Paint selection and expansion before the host synchronously mounts another conversation.
|
|
466
|
+
// Already-expanded workspaces need this too: queueScan has not applied selection yet.
|
|
467
|
+
navigationFrame = window.requestAnimationFrame(() => {
|
|
468
|
+
if (generation !== navigationGeneration) return
|
|
469
|
+
navigationFrame = window.requestAnimationFrame(navigate)
|
|
470
|
+
})
|
|
254
471
|
}
|
|
255
472
|
|
|
256
473
|
function statusSignal(document, state) {
|
|
257
|
-
const signal =
|
|
474
|
+
const signal = state === 'ongoing'
|
|
475
|
+
? document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
476
|
+
: document.createElement('span')
|
|
477
|
+
if (state === 'ongoing') {
|
|
478
|
+
signal.setAttribute('viewBox', '0 0 24 24')
|
|
479
|
+
signal.append(createRunningMarker(document))
|
|
480
|
+
}
|
|
258
481
|
signal.setAttribute('data-prts-session-summary-signal', state)
|
|
259
482
|
signal.setAttribute('aria-hidden', 'true')
|
|
260
483
|
return signal
|
|
@@ -263,6 +486,11 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
263
486
|
function buildSummaryGroup(state, label, items) {
|
|
264
487
|
const group = document.createElement('span')
|
|
265
488
|
group.setAttribute('data-prts-session-summary-group', state)
|
|
489
|
+
group.addEventListener('pointerenter', () => keepSummaryHover(group))
|
|
490
|
+
group.addEventListener('pointerleave', () => {
|
|
491
|
+
window.clearTimeout(summaryLeaveTimer)
|
|
492
|
+
summaryLeaveTimer = window.setTimeout(() => { if (hoveredSummary === group) clearSummaryHover() }, 180)
|
|
493
|
+
})
|
|
266
494
|
|
|
267
495
|
const trigger = document.createElement('button')
|
|
268
496
|
trigger.type = 'button'
|
|
@@ -275,11 +503,19 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
275
503
|
count.textContent = String(items.length)
|
|
276
504
|
trigger.append(count)
|
|
277
505
|
trigger.addEventListener('pointerenter', () => enterPreviewState(state))
|
|
278
|
-
trigger.addEventListener('focus', () =>
|
|
506
|
+
trigger.addEventListener('focus', () => {
|
|
507
|
+
if (trigger.matches(':focus-visible')) {
|
|
508
|
+
summaryDismissed = false
|
|
509
|
+
summaryRoot?.removeAttribute('data-prts-summary-dismissed')
|
|
510
|
+
document.removeEventListener('pointermove', resumeSummaryHover)
|
|
511
|
+
}
|
|
512
|
+
enterPreviewState(state)
|
|
513
|
+
})
|
|
279
514
|
|
|
280
515
|
const popover = document.createElement('span')
|
|
281
516
|
popover.setAttribute('data-prts-session-summary-popover', state)
|
|
282
517
|
popover.setAttribute('role', 'menu')
|
|
518
|
+
popover.setAttribute('popover', 'manual')
|
|
283
519
|
popover.setAttribute('aria-label', label)
|
|
284
520
|
const heading = document.createElement('span')
|
|
285
521
|
heading.setAttribute('data-prts-session-summary-heading', '')
|
|
@@ -302,8 +538,65 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
302
538
|
const list = document.createElement('span')
|
|
303
539
|
list.setAttribute('data-prts-summary-list', '')
|
|
304
540
|
popover.append(list)
|
|
541
|
+
const spaceNotice = document.createElement('div')
|
|
542
|
+
spaceNotice.setAttribute('data-prts-summary-space-notice', '')
|
|
543
|
+
spaceNotice.textContent = '空间不足,无法显示会话预览。建议扩大窗口,或在辅助功能中关闭预览。'
|
|
544
|
+
popover.append(spaceNotice)
|
|
545
|
+
let boundsFrame
|
|
546
|
+
const checkBounds = () => {
|
|
547
|
+
window.cancelAnimationFrame(boundsFrame)
|
|
548
|
+
boundsFrame = undefined
|
|
549
|
+
if (!group.isConnected) return
|
|
550
|
+
// Escape the host sidebar's clipped region without changing its layout.
|
|
551
|
+
if (typeof popover.showPopover === 'function' && !popover.matches(':popover-open')) popover.showPopover()
|
|
552
|
+
const header = group.closest('[data-prts-session-summary-header]')?.getBoundingClientRect()
|
|
553
|
+
if (!header) return
|
|
554
|
+
Object.assign(popover.style, {position:'fixed', boxSizing:'border-box', margin:'0', left:header.left+'px',
|
|
555
|
+
width:header.width+'px', right:'auto', bottom:'auto'})
|
|
556
|
+
popover.removeAttribute('data-prts-summary-no-space')
|
|
557
|
+
popover.removeAttribute('data-prts-summary-no-room')
|
|
558
|
+
const viewport = window.visualViewport
|
|
559
|
+
const left = viewport?.offsetLeft ?? 0, top = viewport?.offsetTop ?? 0
|
|
560
|
+
const right = left + (viewport?.width ?? window.innerWidth) - 8
|
|
561
|
+
const bottom = top + (viewport?.height ?? window.innerHeight) - 8
|
|
562
|
+
const fits = rect => rect.left >= left + 8 && rect.top >= top + 8 && rect.right <= right && rect.bottom <= bottom
|
|
563
|
+
// Measure the final layout, independent of the opening transform animation.
|
|
564
|
+
const place = () => {
|
|
565
|
+
popover.removeAttribute('data-prts-summary-above')
|
|
566
|
+
const parent = header
|
|
567
|
+
const rect = popover.getBoundingClientRect()
|
|
568
|
+
const height = popover.offsetHeight
|
|
569
|
+
const below = parent.bottom + 6
|
|
570
|
+
const above = parent.top - 6 - height
|
|
571
|
+
const candidate = y => ({left: rect.left, right: rect.right, top: y, bottom: y + height})
|
|
572
|
+
popover.style.top = below + 'px'
|
|
573
|
+
if (fits(candidate(below))) return true
|
|
574
|
+
popover.setAttribute('data-prts-summary-above', '')
|
|
575
|
+
if (fits(candidate(above))) { popover.style.top = above + 'px'; return true }
|
|
576
|
+
popover.removeAttribute('data-prts-summary-above')
|
|
577
|
+
return false
|
|
578
|
+
}
|
|
579
|
+
if (place()) { trigger.removeAttribute('title'); return }
|
|
580
|
+
popover.setAttribute('data-prts-summary-no-space', '')
|
|
581
|
+
trigger.title = spaceNotice.textContent
|
|
582
|
+
if (!place()) popover.setAttribute('data-prts-summary-no-room', '')
|
|
583
|
+
}
|
|
584
|
+
const scheduleBounds = () => {
|
|
585
|
+
if (boundsFrame === undefined) boundsFrame = window.requestAnimationFrame(checkBounds)
|
|
586
|
+
}
|
|
587
|
+
const boundsObserver = window.ResizeObserver ? new window.ResizeObserver(scheduleBounds) : undefined
|
|
588
|
+
boundsObserver?.observe(popover)
|
|
589
|
+
// Re-entering the trigger need not re-enter its group (the list is a descendant).
|
|
590
|
+
// Run after enterPreviewState so measurement sees the current status list.
|
|
591
|
+
trigger.addEventListener('pointerenter', checkBounds)
|
|
592
|
+
trigger.addEventListener('focus', checkBounds)
|
|
593
|
+
window.addEventListener('resize', scheduleBounds)
|
|
594
|
+
document.addEventListener('scroll', scheduleBounds, true)
|
|
595
|
+
window.visualViewport?.addEventListener('resize', scheduleBounds)
|
|
596
|
+
window.visualViewport?.addEventListener('scroll', scheduleBounds)
|
|
305
597
|
const entries = new Map()
|
|
306
598
|
const update = items => {
|
|
599
|
+
scheduleBounds()
|
|
307
600
|
count.textContent = String(items.length)
|
|
308
601
|
trigger.setAttribute('aria-label', `${label} ${items.length}`)
|
|
309
602
|
const ids = new Set(items.map(item => item.id))
|
|
@@ -349,6 +642,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
349
642
|
button.setAttribute('aria-label', item.title)
|
|
350
643
|
stopTitles.set(button, scrollSummaryTitle(button, title, text, window))
|
|
351
644
|
const showReply = () => {
|
|
645
|
+
if (summaryDismissed) return
|
|
352
646
|
const key = {ongoing:'previewOngoing',warning:'previewWarning',done:'previewDone'}[state]
|
|
353
647
|
if (previewPreferences[key] === false) { preview.stop(); return }
|
|
354
648
|
preview.show(item.id, reply, item.title, button)
|
|
@@ -374,7 +668,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
374
668
|
}
|
|
375
669
|
update(items)
|
|
376
670
|
summaryGroups.set(state, {group, update, dispose: () => {
|
|
671
|
+
boundsObserver?.disconnect()
|
|
672
|
+
window.cancelAnimationFrame(boundsFrame)
|
|
673
|
+
window.removeEventListener('resize', scheduleBounds)
|
|
674
|
+
document.removeEventListener('scroll', scheduleBounds, true)
|
|
675
|
+
window.visualViewport?.removeEventListener('resize', scheduleBounds)
|
|
676
|
+
window.visualViewport?.removeEventListener('scroll', scheduleBounds)
|
|
377
677
|
for (const {button} of entries.values()) { stopTitles.get(button)?.(); stopTitles.delete(button) }
|
|
678
|
+
if (hoveredSummary === group) clearSummaryHover()
|
|
378
679
|
group.remove()
|
|
379
680
|
}})
|
|
380
681
|
popover.append(reply)
|
|
@@ -457,6 +758,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
457
758
|
}
|
|
458
759
|
const labelHidden = Array.from(label.classList).some(name => /hidden/i.test(name))
|
|
459
760
|
summaryRoot.toggleAttribute('data-prts-summary-search-hidden', labelHidden)
|
|
761
|
+
summaryRoot.toggleAttribute('data-prts-summary-dismissed', summaryDismissed)
|
|
460
762
|
summaryRoot.hidden = warning.length === 0 && ongoing.length === 0 && done.length === 0
|
|
461
763
|
if (signature === summarySignature) return
|
|
462
764
|
summarySignature = signature
|
|
@@ -535,7 +837,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
535
837
|
|
|
536
838
|
|
|
537
839
|
function queueScan() {
|
|
538
|
-
if (scanFrame !== undefined) return
|
|
840
|
+
if (!adapterActive || scanFrame !== undefined) return
|
|
539
841
|
const requestFrame = window?.requestAnimationFrame?.bind(window) ?? (callback => window?.setTimeout?.(callback, 0))
|
|
540
842
|
scanFrame = requestFrame(() => {
|
|
541
843
|
scanFrame = undefined
|
|
@@ -544,7 +846,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
544
846
|
}
|
|
545
847
|
|
|
546
848
|
function scan() {
|
|
547
|
-
|
|
849
|
+
if (!adapterActive) return
|
|
850
|
+
syncSummarySources()
|
|
548
851
|
const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
|
|
549
852
|
const next = new Set()
|
|
550
853
|
const nextVectorOwners = new Set()
|
|
@@ -573,16 +876,31 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
573
876
|
registerVector(controls.menu, { kind: 'button' }, next, nextVectorOwners)
|
|
574
877
|
registerVector(controls.create, { kind: 'button' }, next, nextVectorOwners)
|
|
575
878
|
}
|
|
879
|
+
const rowSessionId = row => row.getAttribute('data-session-id') || row.getAttribute('data-row-key')?.replace(/^session:/, '')
|
|
880
|
+
if (selectionTarget && Array.from(root.querySelectorAll('[role="treeitem"][aria-selected="true"]')).some(row => rowSessionId(row) === selectionTarget)) clearSelectionTarget()
|
|
576
881
|
let sessionOrdinal = 0
|
|
577
882
|
for (const row of root.querySelectorAll('div[role="treeitem"][aria-selected]')) {
|
|
578
883
|
const sessionIndex = String(++sessionOrdinal).padStart(2, '0')
|
|
579
884
|
const controls = resolveRowControls(row, 'session')
|
|
580
885
|
const state = nativeSessionState(row)
|
|
886
|
+
const spinner = state === 'ongoing' ? row.querySelector('svg[data-state="ongoing"]') : undefined
|
|
887
|
+
if (spinner) {
|
|
888
|
+
let marker = spinner.querySelector('[data-prts-running-marker]')
|
|
889
|
+
if (!marker) {
|
|
890
|
+
marker = createRunningMarker(document)
|
|
891
|
+
spinner.append(marker)
|
|
892
|
+
}
|
|
893
|
+
next.add(marker)
|
|
894
|
+
}
|
|
895
|
+
|
|
581
896
|
const face = ensureFacilityFace(document, row, 'session', next)
|
|
582
897
|
const spine = ensureFacilitySpine(document, row, 'session', next)
|
|
583
898
|
registerVector(face, { kind: 'face', topNotch: true, sessionNotch: true, texture: 'pickup' }, next, nextVectorOwners)
|
|
584
899
|
registerVector(spine, { kind: 'spine' }, next, nextVectorOwners)
|
|
585
|
-
|
|
900
|
+
const selected = selectionTarget ? rowSessionId(row) === selectionTarget : row.getAttribute('aria-selected') === 'true'
|
|
901
|
+
if (selectionTarget) row.setAttribute('data-prts-selection-target', String(selected))
|
|
902
|
+
else row.removeAttribute('data-prts-selection-target')
|
|
903
|
+
if (!activeLifelineRow && selected) {
|
|
586
904
|
activeLifelineRow = row
|
|
587
905
|
activeLifeline = face.querySelector('[data-prts-session-lifeline]')
|
|
588
906
|
}
|
|
@@ -617,7 +935,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
617
935
|
lifelineAnimator.setTarget(activeLifelineRow, activeLifeline)
|
|
618
936
|
for (const node of owned) {
|
|
619
937
|
if (next.has(node)) continue
|
|
620
|
-
if (node.hasAttribute?.('data-prts-
|
|
938
|
+
if (node.hasAttribute?.('data-prts-running-marker')
|
|
939
|
+
|| node.hasAttribute?.('data-prts-owned-projection')
|
|
621
940
|
|| node.hasAttribute?.('data-prts-owned-facility')
|
|
622
941
|
|| node.hasAttribute?.('data-prts-owned-facility-spine')
|
|
623
942
|
|| node.hasAttribute?.('data-prts-owned-facility-vector')) {
|
|
@@ -628,13 +947,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
628
947
|
}
|
|
629
948
|
syncVectorOwners(nextVectorOwners)
|
|
630
949
|
owned = next
|
|
950
|
+
syncSessionReveal(root)
|
|
631
951
|
}
|
|
632
952
|
|
|
633
953
|
function clear() {
|
|
634
954
|
preview.stop()
|
|
635
955
|
clearSummaryEffects()
|
|
636
956
|
lifelineAnimator.setTarget(undefined, undefined)
|
|
637
|
-
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()
|
|
957
|
+
for (const node of document?.querySelectorAll?.('[data-prts-running-marker], [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()
|
|
638
958
|
for (const node of owned) {
|
|
639
959
|
for (const attribute of OWNED_ATTRIBUTES) node.removeAttribute?.(attribute)
|
|
640
960
|
}
|
|
@@ -655,6 +975,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
655
975
|
if (keys.some(key => previewPreferences[key] !== preferences[key])) preview.stop()
|
|
656
976
|
previewPreferences = Object.fromEntries(keys.map(key => [key, preferences[key]]))
|
|
657
977
|
if (!document) return
|
|
978
|
+
adapterActive = true
|
|
658
979
|
lifelineAnimator.start()
|
|
659
980
|
ensureFacilityTextureDefs(document)
|
|
660
981
|
const ResizeObserver = window?.ResizeObserver
|
|
@@ -669,19 +990,23 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
669
990
|
flushVectors()
|
|
670
991
|
})
|
|
671
992
|
}
|
|
672
|
-
document.addEventListener('pointerdown', dismissHeldPreview)
|
|
673
|
-
document.addEventListener('keydown', dismissHeldPreview)
|
|
674
993
|
scan()
|
|
675
|
-
if (!statusSourcesStarted) {
|
|
676
|
-
statusSourcesStarted = true
|
|
677
|
-
stopSessionList = sessions?.list?.subscribe?.(queueScan) ?? (() => {})
|
|
678
|
-
stopWorkspaceList = workspaces?.list?.subscribe?.(queueScan) ?? (() => {})
|
|
679
|
-
}
|
|
680
994
|
if (observer || !window?.MutationObserver) return
|
|
681
995
|
const observationRoot = document.querySelector('[data-prts-region="sessions"]')
|
|
682
996
|
if (!observationRoot) return
|
|
683
997
|
observer = new window.MutationObserver(mutations => {
|
|
684
|
-
|
|
998
|
+
const relevant = mutations.filter(mutation => !mutation.target.closest?.('[data-prts-owned-session-summary]'))
|
|
999
|
+
const freshRow = 'div[role="treeitem"]:not([data-prts-session-row]):not([data-prts-workspace-row])'
|
|
1000
|
+
const mountsRows = relevant.some(mutation => mutation.type === 'childList'
|
|
1001
|
+
&& Array.from(mutation.addedNodes).some(node => node.nodeType === 1
|
|
1002
|
+
&& (node.matches(freshRow) || node.querySelector(freshRow))))
|
|
1003
|
+
if (mountsRows) {
|
|
1004
|
+
// React has mounted expanded rows. Finish theme geometry before their first paint.
|
|
1005
|
+
window.cancelAnimationFrame?.(scanFrame)
|
|
1006
|
+
scanFrame = undefined
|
|
1007
|
+
scan()
|
|
1008
|
+
flushVectors()
|
|
1009
|
+
} else if (relevant.length) queueScan()
|
|
685
1010
|
})
|
|
686
1011
|
observer.observe(observationRoot, {
|
|
687
1012
|
subtree: true,
|
|
@@ -691,19 +1016,16 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
691
1016
|
})
|
|
692
1017
|
},
|
|
693
1018
|
dispose() {
|
|
1019
|
+
adapterActive = false
|
|
1020
|
+
disconnectSummarySources()
|
|
1021
|
+
cancelReveal()
|
|
1022
|
+
document.removeEventListener('pointermove', resumeSummaryHover)
|
|
1023
|
+
summaryDismissed = false
|
|
694
1024
|
heldPreviewState = undefined
|
|
695
1025
|
document?.removeEventListener('pointerdown', dismissHeldPreview)
|
|
696
1026
|
document?.removeEventListener('keydown', dismissHeldPreview)
|
|
697
1027
|
observer?.disconnect()
|
|
698
1028
|
observer = undefined
|
|
699
|
-
stopSessionList()
|
|
700
|
-
stopSessionList = () => {}
|
|
701
|
-
stopWorkspaceList()
|
|
702
|
-
stopWorkspaceList = () => {}
|
|
703
|
-
stopSessionStatus()
|
|
704
|
-
stopSessionStatus = () => {}
|
|
705
|
-
sessionStatus = undefined
|
|
706
|
-
statusSourcesStarted = false
|
|
707
1029
|
if (scanFrame !== undefined) {
|
|
708
1030
|
if (window?.cancelAnimationFrame) window.cancelAnimationFrame(scanFrame)
|
|
709
1031
|
else window?.clearTimeout?.(scanFrame)
|