@ash-qw/dsh-theme-prts 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/lib/client.js +472 -80
- package/package.json +2 -2
- 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 +370 -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,12 @@ 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
|
+
const deferHoverClose = () => {
|
|
490
|
+
window.clearTimeout(summaryLeaveTimer)
|
|
491
|
+
summaryLeaveTimer = window.setTimeout(() => { if (hoveredSummary === group) clearSummaryHover() }, 350)
|
|
492
|
+
}
|
|
493
|
+
group.addEventListener('pointerenter', () => keepSummaryHover(group))
|
|
494
|
+
group.addEventListener('pointerleave', deferHoverClose)
|
|
266
495
|
|
|
267
496
|
const trigger = document.createElement('button')
|
|
268
497
|
trigger.type = 'button'
|
|
@@ -275,12 +504,24 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
275
504
|
count.textContent = String(items.length)
|
|
276
505
|
trigger.append(count)
|
|
277
506
|
trigger.addEventListener('pointerenter', () => enterPreviewState(state))
|
|
278
|
-
trigger.addEventListener('focus', () =>
|
|
507
|
+
trigger.addEventListener('focus', () => {
|
|
508
|
+
if (trigger.matches(':focus-visible')) {
|
|
509
|
+
summaryDismissed = false
|
|
510
|
+
summaryRoot?.removeAttribute('data-prts-summary-dismissed')
|
|
511
|
+
document.removeEventListener('pointermove', resumeSummaryHover)
|
|
512
|
+
}
|
|
513
|
+
enterPreviewState(state)
|
|
514
|
+
})
|
|
279
515
|
|
|
280
516
|
const popover = document.createElement('span')
|
|
281
517
|
popover.setAttribute('data-prts-session-summary-popover', state)
|
|
282
518
|
popover.setAttribute('role', 'menu')
|
|
519
|
+
popover.setAttribute('popover', 'manual')
|
|
283
520
|
popover.setAttribute('aria-label', label)
|
|
521
|
+
// The popover lives in the browser top layer; crossing into it can leave
|
|
522
|
+
// the trigger's hit area even though it remains a DOM descendant.
|
|
523
|
+
popover.addEventListener('pointerenter', () => keepSummaryHover(group))
|
|
524
|
+
popover.addEventListener('pointerleave', deferHoverClose)
|
|
284
525
|
const heading = document.createElement('span')
|
|
285
526
|
heading.setAttribute('data-prts-session-summary-heading', '')
|
|
286
527
|
const headingLabel = document.createElement('strong')
|
|
@@ -302,8 +543,65 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
302
543
|
const list = document.createElement('span')
|
|
303
544
|
list.setAttribute('data-prts-summary-list', '')
|
|
304
545
|
popover.append(list)
|
|
546
|
+
const spaceNotice = document.createElement('div')
|
|
547
|
+
spaceNotice.setAttribute('data-prts-summary-space-notice', '')
|
|
548
|
+
spaceNotice.textContent = '空间不足,无法显示会话预览。建议扩大窗口,或在辅助功能中关闭预览。'
|
|
549
|
+
popover.append(spaceNotice)
|
|
550
|
+
let boundsFrame
|
|
551
|
+
const checkBounds = () => {
|
|
552
|
+
window.cancelAnimationFrame(boundsFrame)
|
|
553
|
+
boundsFrame = undefined
|
|
554
|
+
if (!group.isConnected) return
|
|
555
|
+
// Escape the host sidebar's clipped region without changing its layout.
|
|
556
|
+
if (typeof popover.showPopover === 'function' && !popover.matches(':popover-open')) popover.showPopover()
|
|
557
|
+
const header = group.closest('[data-prts-session-summary-header]')?.getBoundingClientRect()
|
|
558
|
+
if (!header) return
|
|
559
|
+
Object.assign(popover.style, {position:'fixed', boxSizing:'border-box', margin:'0', left:header.left+'px',
|
|
560
|
+
width:header.width+'px', right:'auto', bottom:'auto'})
|
|
561
|
+
popover.removeAttribute('data-prts-summary-no-space')
|
|
562
|
+
popover.removeAttribute('data-prts-summary-no-room')
|
|
563
|
+
const viewport = window.visualViewport
|
|
564
|
+
const left = viewport?.offsetLeft ?? 0, top = viewport?.offsetTop ?? 0
|
|
565
|
+
const right = left + (viewport?.width ?? window.innerWidth) - 8
|
|
566
|
+
const bottom = top + (viewport?.height ?? window.innerHeight) - 8
|
|
567
|
+
const fits = rect => rect.left >= left + 8 && rect.top >= top + 8 && rect.right <= right && rect.bottom <= bottom
|
|
568
|
+
// Measure the final layout, independent of the opening transform animation.
|
|
569
|
+
const place = () => {
|
|
570
|
+
popover.removeAttribute('data-prts-summary-above')
|
|
571
|
+
const parent = header
|
|
572
|
+
const rect = popover.getBoundingClientRect()
|
|
573
|
+
const height = popover.offsetHeight
|
|
574
|
+
const below = parent.bottom + 6
|
|
575
|
+
const above = parent.top - 6 - height
|
|
576
|
+
const candidate = y => ({left: rect.left, right: rect.right, top: y, bottom: y + height})
|
|
577
|
+
popover.style.top = below + 'px'
|
|
578
|
+
if (fits(candidate(below))) return true
|
|
579
|
+
popover.setAttribute('data-prts-summary-above', '')
|
|
580
|
+
if (fits(candidate(above))) { popover.style.top = above + 'px'; return true }
|
|
581
|
+
popover.removeAttribute('data-prts-summary-above')
|
|
582
|
+
return false
|
|
583
|
+
}
|
|
584
|
+
if (place()) { trigger.removeAttribute('title'); return }
|
|
585
|
+
popover.setAttribute('data-prts-summary-no-space', '')
|
|
586
|
+
trigger.title = spaceNotice.textContent
|
|
587
|
+
if (!place()) popover.setAttribute('data-prts-summary-no-room', '')
|
|
588
|
+
}
|
|
589
|
+
const scheduleBounds = () => {
|
|
590
|
+
if (boundsFrame === undefined) boundsFrame = window.requestAnimationFrame(checkBounds)
|
|
591
|
+
}
|
|
592
|
+
const boundsObserver = window.ResizeObserver ? new window.ResizeObserver(scheduleBounds) : undefined
|
|
593
|
+
boundsObserver?.observe(popover)
|
|
594
|
+
// Re-entering the trigger need not re-enter its group (the list is a descendant).
|
|
595
|
+
// Run after enterPreviewState so measurement sees the current status list.
|
|
596
|
+
trigger.addEventListener('pointerenter', checkBounds)
|
|
597
|
+
trigger.addEventListener('focus', checkBounds)
|
|
598
|
+
window.addEventListener('resize', scheduleBounds)
|
|
599
|
+
document.addEventListener('scroll', scheduleBounds, true)
|
|
600
|
+
window.visualViewport?.addEventListener('resize', scheduleBounds)
|
|
601
|
+
window.visualViewport?.addEventListener('scroll', scheduleBounds)
|
|
305
602
|
const entries = new Map()
|
|
306
603
|
const update = items => {
|
|
604
|
+
scheduleBounds()
|
|
307
605
|
count.textContent = String(items.length)
|
|
308
606
|
trigger.setAttribute('aria-label', `${label} ${items.length}`)
|
|
309
607
|
const ids = new Set(items.map(item => item.id))
|
|
@@ -349,6 +647,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
349
647
|
button.setAttribute('aria-label', item.title)
|
|
350
648
|
stopTitles.set(button, scrollSummaryTitle(button, title, text, window))
|
|
351
649
|
const showReply = () => {
|
|
650
|
+
if (summaryDismissed) return
|
|
352
651
|
const key = {ongoing:'previewOngoing',warning:'previewWarning',done:'previewDone'}[state]
|
|
353
652
|
if (previewPreferences[key] === false) { preview.stop(); return }
|
|
354
653
|
preview.show(item.id, reply, item.title, button)
|
|
@@ -374,7 +673,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
374
673
|
}
|
|
375
674
|
update(items)
|
|
376
675
|
summaryGroups.set(state, {group, update, dispose: () => {
|
|
676
|
+
boundsObserver?.disconnect()
|
|
677
|
+
window.cancelAnimationFrame(boundsFrame)
|
|
678
|
+
window.removeEventListener('resize', scheduleBounds)
|
|
679
|
+
document.removeEventListener('scroll', scheduleBounds, true)
|
|
680
|
+
window.visualViewport?.removeEventListener('resize', scheduleBounds)
|
|
681
|
+
window.visualViewport?.removeEventListener('scroll', scheduleBounds)
|
|
377
682
|
for (const {button} of entries.values()) { stopTitles.get(button)?.(); stopTitles.delete(button) }
|
|
683
|
+
if (hoveredSummary === group) clearSummaryHover()
|
|
378
684
|
group.remove()
|
|
379
685
|
}})
|
|
380
686
|
popover.append(reply)
|
|
@@ -457,6 +763,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
457
763
|
}
|
|
458
764
|
const labelHidden = Array.from(label.classList).some(name => /hidden/i.test(name))
|
|
459
765
|
summaryRoot.toggleAttribute('data-prts-summary-search-hidden', labelHidden)
|
|
766
|
+
summaryRoot.toggleAttribute('data-prts-summary-dismissed', summaryDismissed)
|
|
460
767
|
summaryRoot.hidden = warning.length === 0 && ongoing.length === 0 && done.length === 0
|
|
461
768
|
if (signature === summarySignature) return
|
|
462
769
|
summarySignature = signature
|
|
@@ -535,7 +842,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
535
842
|
|
|
536
843
|
|
|
537
844
|
function queueScan() {
|
|
538
|
-
if (scanFrame !== undefined) return
|
|
845
|
+
if (!adapterActive || scanFrame !== undefined) return
|
|
539
846
|
const requestFrame = window?.requestAnimationFrame?.bind(window) ?? (callback => window?.setTimeout?.(callback, 0))
|
|
540
847
|
scanFrame = requestFrame(() => {
|
|
541
848
|
scanFrame = undefined
|
|
@@ -544,7 +851,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
544
851
|
}
|
|
545
852
|
|
|
546
853
|
function scan() {
|
|
547
|
-
|
|
854
|
+
if (!adapterActive) return
|
|
855
|
+
syncSummarySources()
|
|
548
856
|
const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
|
|
549
857
|
const next = new Set()
|
|
550
858
|
const nextVectorOwners = new Set()
|
|
@@ -573,16 +881,31 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
573
881
|
registerVector(controls.menu, { kind: 'button' }, next, nextVectorOwners)
|
|
574
882
|
registerVector(controls.create, { kind: 'button' }, next, nextVectorOwners)
|
|
575
883
|
}
|
|
884
|
+
const rowSessionId = row => row.getAttribute('data-session-id') || row.getAttribute('data-row-key')?.replace(/^session:/, '')
|
|
885
|
+
if (selectionTarget && Array.from(root.querySelectorAll('[role="treeitem"][aria-selected="true"]')).some(row => rowSessionId(row) === selectionTarget)) clearSelectionTarget()
|
|
576
886
|
let sessionOrdinal = 0
|
|
577
887
|
for (const row of root.querySelectorAll('div[role="treeitem"][aria-selected]')) {
|
|
578
888
|
const sessionIndex = String(++sessionOrdinal).padStart(2, '0')
|
|
579
889
|
const controls = resolveRowControls(row, 'session')
|
|
580
890
|
const state = nativeSessionState(row)
|
|
891
|
+
const spinner = state === 'ongoing' ? row.querySelector('svg[data-state="ongoing"]') : undefined
|
|
892
|
+
if (spinner) {
|
|
893
|
+
let marker = spinner.querySelector('[data-prts-running-marker]')
|
|
894
|
+
if (!marker) {
|
|
895
|
+
marker = createRunningMarker(document)
|
|
896
|
+
spinner.append(marker)
|
|
897
|
+
}
|
|
898
|
+
next.add(marker)
|
|
899
|
+
}
|
|
900
|
+
|
|
581
901
|
const face = ensureFacilityFace(document, row, 'session', next)
|
|
582
902
|
const spine = ensureFacilitySpine(document, row, 'session', next)
|
|
583
903
|
registerVector(face, { kind: 'face', topNotch: true, sessionNotch: true, texture: 'pickup' }, next, nextVectorOwners)
|
|
584
904
|
registerVector(spine, { kind: 'spine' }, next, nextVectorOwners)
|
|
585
|
-
|
|
905
|
+
const selected = selectionTarget ? rowSessionId(row) === selectionTarget : row.getAttribute('aria-selected') === 'true'
|
|
906
|
+
if (selectionTarget) row.setAttribute('data-prts-selection-target', String(selected))
|
|
907
|
+
else row.removeAttribute('data-prts-selection-target')
|
|
908
|
+
if (!activeLifelineRow && selected) {
|
|
586
909
|
activeLifelineRow = row
|
|
587
910
|
activeLifeline = face.querySelector('[data-prts-session-lifeline]')
|
|
588
911
|
}
|
|
@@ -617,7 +940,8 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
617
940
|
lifelineAnimator.setTarget(activeLifelineRow, activeLifeline)
|
|
618
941
|
for (const node of owned) {
|
|
619
942
|
if (next.has(node)) continue
|
|
620
|
-
if (node.hasAttribute?.('data-prts-
|
|
943
|
+
if (node.hasAttribute?.('data-prts-running-marker')
|
|
944
|
+
|| node.hasAttribute?.('data-prts-owned-projection')
|
|
621
945
|
|| node.hasAttribute?.('data-prts-owned-facility')
|
|
622
946
|
|| node.hasAttribute?.('data-prts-owned-facility-spine')
|
|
623
947
|
|| node.hasAttribute?.('data-prts-owned-facility-vector')) {
|
|
@@ -628,13 +952,14 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
628
952
|
}
|
|
629
953
|
syncVectorOwners(nextVectorOwners)
|
|
630
954
|
owned = next
|
|
955
|
+
syncSessionReveal(root)
|
|
631
956
|
}
|
|
632
957
|
|
|
633
958
|
function clear() {
|
|
634
959
|
preview.stop()
|
|
635
960
|
clearSummaryEffects()
|
|
636
961
|
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()
|
|
962
|
+
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
963
|
for (const node of owned) {
|
|
639
964
|
for (const attribute of OWNED_ATTRIBUTES) node.removeAttribute?.(attribute)
|
|
640
965
|
}
|
|
@@ -655,6 +980,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
655
980
|
if (keys.some(key => previewPreferences[key] !== preferences[key])) preview.stop()
|
|
656
981
|
previewPreferences = Object.fromEntries(keys.map(key => [key, preferences[key]]))
|
|
657
982
|
if (!document) return
|
|
983
|
+
adapterActive = true
|
|
658
984
|
lifelineAnimator.start()
|
|
659
985
|
ensureFacilityTextureDefs(document)
|
|
660
986
|
const ResizeObserver = window?.ResizeObserver
|
|
@@ -669,19 +995,23 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
669
995
|
flushVectors()
|
|
670
996
|
})
|
|
671
997
|
}
|
|
672
|
-
document.addEventListener('pointerdown', dismissHeldPreview)
|
|
673
|
-
document.addEventListener('keydown', dismissHeldPreview)
|
|
674
998
|
scan()
|
|
675
|
-
if (!statusSourcesStarted) {
|
|
676
|
-
statusSourcesStarted = true
|
|
677
|
-
stopSessionList = sessions?.list?.subscribe?.(queueScan) ?? (() => {})
|
|
678
|
-
stopWorkspaceList = workspaces?.list?.subscribe?.(queueScan) ?? (() => {})
|
|
679
|
-
}
|
|
680
999
|
if (observer || !window?.MutationObserver) return
|
|
681
1000
|
const observationRoot = document.querySelector('[data-prts-region="sessions"]')
|
|
682
1001
|
if (!observationRoot) return
|
|
683
1002
|
observer = new window.MutationObserver(mutations => {
|
|
684
|
-
|
|
1003
|
+
const relevant = mutations.filter(mutation => !mutation.target.closest?.('[data-prts-owned-session-summary]'))
|
|
1004
|
+
const freshRow = 'div[role="treeitem"]:not([data-prts-session-row]):not([data-prts-workspace-row])'
|
|
1005
|
+
const mountsRows = relevant.some(mutation => mutation.type === 'childList'
|
|
1006
|
+
&& Array.from(mutation.addedNodes).some(node => node.nodeType === 1
|
|
1007
|
+
&& (node.matches(freshRow) || node.querySelector(freshRow))))
|
|
1008
|
+
if (mountsRows) {
|
|
1009
|
+
// React has mounted expanded rows. Finish theme geometry before their first paint.
|
|
1010
|
+
window.cancelAnimationFrame?.(scanFrame)
|
|
1011
|
+
scanFrame = undefined
|
|
1012
|
+
scan()
|
|
1013
|
+
flushVectors()
|
|
1014
|
+
} else if (relevant.length) queueScan()
|
|
685
1015
|
})
|
|
686
1016
|
observer.observe(observationRoot, {
|
|
687
1017
|
subtree: true,
|
|
@@ -691,19 +1021,16 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
691
1021
|
})
|
|
692
1022
|
},
|
|
693
1023
|
dispose() {
|
|
1024
|
+
adapterActive = false
|
|
1025
|
+
disconnectSummarySources()
|
|
1026
|
+
cancelReveal()
|
|
1027
|
+
document.removeEventListener('pointermove', resumeSummaryHover)
|
|
1028
|
+
summaryDismissed = false
|
|
694
1029
|
heldPreviewState = undefined
|
|
695
1030
|
document?.removeEventListener('pointerdown', dismissHeldPreview)
|
|
696
1031
|
document?.removeEventListener('keydown', dismissHeldPreview)
|
|
697
1032
|
observer?.disconnect()
|
|
698
1033
|
observer = undefined
|
|
699
|
-
stopSessionList()
|
|
700
|
-
stopSessionList = () => {}
|
|
701
|
-
stopWorkspaceList()
|
|
702
|
-
stopWorkspaceList = () => {}
|
|
703
|
-
stopSessionStatus()
|
|
704
|
-
stopSessionStatus = () => {}
|
|
705
|
-
sessionStatus = undefined
|
|
706
|
-
statusSourcesStarted = false
|
|
707
1034
|
if (scanFrame !== undefined) {
|
|
708
1035
|
if (window?.cancelAnimationFrame) window.cancelAnimationFrame(scanFrame)
|
|
709
1036
|
else window?.clearTimeout?.(scanFrame)
|