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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-qw/dsh-theme-prts",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "engines": {
70
70
  "node": ">=18",
71
- "dsh": ">=0.1.2-rc.1 <0.1.3-0"
71
+ "dsh": ">=0.1.2-rc.1 <0.1.3-0 || >=0.1.5-rc.1 <0.1.6-0"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@playwright/test": "1.55.1",
@@ -1,5 +1,6 @@
1
1
  export const ASSISTANT_SURFACE_ATTRIBUTE = 'data-prts-ai-surface'
2
2
  export const ASSISTANT_STATE_ATTRIBUTE = 'data-prts-ai-surface-state'
3
+ export const ASSISTANT_AVATAR_PROPERTY = '--prts-assistant-avatar-image'
3
4
 
4
5
  const STEP_SELECTOR = '[data-chat-flow-kind="assistant-step"]'
5
6
  const CONTENT_HINT_SELECTOR = [
@@ -16,6 +17,7 @@ const EXCLUDED_SELECTOR = [
16
17
  ].join(', ')
17
18
  const SCAN_CHUNK_LIMIT = 64
18
19
  const SCAN_BUDGET_MS = 4
20
+ const INITIAL_LATEST_LIMIT = 12
19
21
 
20
22
  function isElement(node) {
21
23
  return node?.nodeType === 1
@@ -90,13 +92,42 @@ export function resolveAssistantSurface(step) {
90
92
  return resolution(step).surface
91
93
  }
92
94
 
93
- export function createAssistantGlassAdapter({ document, window }) {
95
+ function cssUrl(value) {
96
+ if (!value) return ''
97
+ const escaped = String(value)
98
+ .replaceAll('\\', '\\\\')
99
+ .replaceAll('"', '\\"')
100
+ .replaceAll('\n', '')
101
+ return `url("${escaped}")`
102
+ }
103
+
104
+ export function createAssistantGlassAdapter({ document, window, avatarImage = '' }) {
94
105
  let observer
95
106
  let scanFrame
96
107
  const ownedSurfaces = new Set()
97
108
  const ownedSteps = new Set()
98
109
  const stepSurfaces = new Map()
99
110
  const pendingSteps = new Set()
111
+ let previousAvatarImage
112
+ let ownsAvatarImage = false
113
+
114
+ function mountAvatarImage() {
115
+ const root = document?.documentElement
116
+ const value = cssUrl(avatarImage)
117
+ if (!root?.style || !value || ownsAvatarImage) return
118
+ previousAvatarImage = root.style.getPropertyValue(ASSISTANT_AVATAR_PROPERTY)
119
+ root.style.setProperty(ASSISTANT_AVATAR_PROPERTY, value)
120
+ ownsAvatarImage = true
121
+ }
122
+
123
+ function releaseAvatarImage() {
124
+ if (!ownsAvatarImage) return
125
+ const root = document?.documentElement
126
+ if (previousAvatarImage) root?.style?.setProperty(ASSISTANT_AVATAR_PROPERTY, previousAvatarImage)
127
+ else root?.style?.removeProperty(ASSISTANT_AVATAR_PROPERTY)
128
+ previousAvatarImage = undefined
129
+ ownsAvatarImage = false
130
+ }
100
131
 
101
132
  function collectSteps(node, includeParent = true) {
102
133
  if (!isElement(node)) return
@@ -108,6 +139,23 @@ export function createAssistantGlassAdapter({ document, window }) {
108
139
  }
109
140
  }
110
141
 
142
+ function collectInitialSteps(node) {
143
+ if (!isElement(node)) return
144
+ const steps = [...(node.querySelectorAll?.(STEP_SELECTOR) ?? [])]
145
+ if (node.matches?.(STEP_SELECTOR)) steps.unshift(node)
146
+ const priorityStart = Math.max(0, steps.length - INITIAL_LATEST_LIMIT)
147
+
148
+ // The host restores conversations in chronological DOM order. Resolve the
149
+ // newest screenful synchronously so the startup cover never reveals plain
150
+ // assistant content while an older history backlog is still being scanned.
151
+ for (let index = steps.length - 1; index >= priorityStart; index -= 1) {
152
+ processStep(steps[index])
153
+ }
154
+ for (let index = priorityStart - 1; index >= 0; index -= 1) {
155
+ pendingSteps.add(steps[index])
156
+ }
157
+ }
158
+
111
159
  function stableBodyContains(step, node) {
112
160
  const surface = stepSurfaces.get(step)
113
161
  return step?.getAttribute?.(ASSISTANT_STATE_ATTRIBUTE) === 'body'
@@ -201,12 +249,13 @@ export function createAssistantGlassAdapter({ document, window }) {
201
249
  return {
202
250
  start() {
203
251
  if (!document) return
252
+ mountAvatarImage()
204
253
  const observationRoot = document.querySelector('[data-prts-region="operation"]')
205
- ?? document.querySelector('[data-slot="conversation"]')
254
+ ?? document.querySelector('[data-slot="main.conversation"], [data-slot="conversation"]')
206
255
  if (!observationRoot) return
207
- collectSteps(observationRoot)
208
- scan()
209
256
  if (observer) return
257
+ collectInitialSteps(observationRoot)
258
+ scan()
210
259
  const Observer = window?.MutationObserver
211
260
  if (!Observer) return
212
261
  observer = new Observer(onMutations)
@@ -226,6 +275,7 @@ export function createAssistantGlassAdapter({ document, window }) {
226
275
  }
227
276
  scanFrame = undefined
228
277
  removeMarkers()
278
+ releaseAvatarImage()
229
279
  },
230
280
  }
231
281
  }
@@ -2,8 +2,13 @@ const CONTROL_ATTRIBUTE = 'data-prts-glass-control'
2
2
  const MENU_ATTRIBUTE = 'data-prts-glass-menu'
3
3
  const FALLBACK_COMPOSER_ATTRIBUTE = 'data-prts-composer-fallback'
4
4
  const COMPOSER_SIGNAL_ATTRIBUTE = 'data-prts-composer-signal'
5
+ const FALLBACK_COMPOSER_ROOT_SELECTOR = [
6
+ '[data-slot="conversation.composer"]',
7
+ '[data-composer-seat]',
8
+ ].join(', ')
5
9
  const COMPOSER_RELEVANCE_SELECTOR = [
6
10
  '[data-composer-card]',
11
+ FALLBACK_COMPOSER_ROOT_SELECTOR,
7
12
  'form textarea',
8
13
  'button[aria-haspopup="menu"]',
9
14
  'button[aria-haspopup="listbox"]',
@@ -56,6 +61,18 @@ function markPopup(popup, kind) {
56
61
  }
57
62
  }
58
63
 
64
+ function fallbackComposerForms(document) {
65
+ const forms = new Set()
66
+ for (const root of document?.querySelectorAll?.(FALLBACK_COMPOSER_ROOT_SELECTOR) ?? []) {
67
+ if (root.querySelector('[data-composer-card]')) continue
68
+ for (const textarea of root.querySelectorAll('form textarea')) {
69
+ const form = textarea.closest('form')
70
+ if (form && root.contains(form)) forms.add(form)
71
+ }
72
+ }
73
+ return forms
74
+ }
75
+
59
76
  export function createComposerGlassAdapter({ document, window }) {
60
77
  let observer
61
78
  let scanFrame
@@ -104,17 +121,17 @@ export function createComposerGlassAdapter({ document, window }) {
104
121
  signal.remove?.()
105
122
  ownedSignals.delete(signal)
106
123
  }
124
+ const fallbackForms = fallbackComposerForms(document)
107
125
  for (const form of [...ownedFallbacks]) {
108
- if (!form.isConnected || form.hasAttribute('data-composer-card')) {
109
- form.removeAttribute?.(FALLBACK_COMPOSER_ATTRIBUTE)
110
- ownedFallbacks.delete(form)
111
- }
126
+ if (form.isConnected && fallbackForms.has(form) && !form.hasAttribute('data-composer-card')) continue
127
+ form.removeAttribute?.(FALLBACK_COMPOSER_ATTRIBUTE)
128
+ ownedFallbacks.delete(form)
129
+ if (form.hasAttribute('data-composer-card')) continue
130
+ const signal = form.querySelector?.(`:scope > [${COMPOSER_SIGNAL_ATTRIBUTE}]`)
131
+ if (!ownedSignals.has(signal)) continue
132
+ signal.remove?.()
133
+ ownedSignals.delete(signal)
112
134
  }
113
- const fallbackForms = new Set(
114
- [...(document?.querySelectorAll?.('form textarea') ?? [])]
115
- .map(textarea => textarea.closest('form'))
116
- .filter(form => form && !form.querySelector('[data-composer-card]')),
117
- )
118
135
  for (const form of fallbackForms) {
119
136
  form.setAttribute(FALLBACK_COMPOSER_ATTRIBUTE, '')
120
137
  ownedFallbacks.add(form)
@@ -187,7 +204,7 @@ export function createComposerGlassAdapter({ document, window }) {
187
204
  const Observer = window?.MutationObserver
188
205
  if (!Observer) return
189
206
  const observationRoot = document.querySelector('[data-prts-region="operation"]')
190
- ?? document.querySelector('[data-slot="conversation"]')
207
+ ?? document.querySelector('[data-slot="main.conversation"], [data-slot="conversation"]')
191
208
  if (!observationRoot) return
192
209
  observer = new Observer(onMutations)
193
210
  observer.observe(observationRoot, {
@@ -44,9 +44,12 @@ export function applyPrtsPlugin(ctx, environment) {
44
44
  const safeMode = isSafeMode(window?.location?.search ?? '')
45
45
  const storage = window?.localStorage
46
46
  const themeService = contextService(ctx, 'theme')
47
- const startup = createPrtsStartupSequence({ document, window, prtsEmblem: assets.prtsEmblem, rhodesEmblem: assets.emblem, timings: startupTimings })
48
47
  const composerGlass = createComposerGlassAdapter({ document, window })
49
- const assistantGlass = createAssistantGlassAdapter({ document, window })
48
+ const assistantGlass = createAssistantGlassAdapter({
49
+ document,
50
+ window,
51
+ avatarImage: assets.prtsEmblem,
52
+ })
50
53
  const particleField = createParticleFieldAdapter({
51
54
  document,
52
55
  window,
@@ -58,6 +61,19 @@ export function applyPrtsPlugin(ctx, environment) {
58
61
  sync()
59
62
  },
60
63
  })
64
+ const startup = createPrtsStartupSequence({
65
+ document,
66
+ window,
67
+ prtsEmblem: assets.prtsEmblem,
68
+ rhodesEmblem: assets.emblem,
69
+ timings: startupTimings,
70
+ onActiveChange(active) {
71
+ particleField.setStartupActive(active)
72
+ },
73
+ onReady() {
74
+ particleField.setStartupActive(false)
75
+ },
76
+ })
61
77
  const theme = createThemeController({
62
78
  document,
63
79
  window,
@@ -140,6 +156,7 @@ export function applyPrtsPlugin(ctx, environment) {
140
156
  }
141
157
 
142
158
  const disposeVisualState = () => {
159
+ particleField.update({ ...preferences, enabled: false })
143
160
  startup.stop({ restoreFocus: false })
144
161
  stopStartupReadyWatch()
145
162
  stopMountWatch()
@@ -147,7 +164,6 @@ export function applyPrtsPlugin(ctx, environment) {
147
164
  disposeOperations()
148
165
  composerGlass.dispose()
149
166
  assistantGlass.dispose()
150
- particleField.update({ ...preferences, enabled: false })
151
167
  toBottom.dispose()
152
168
  conversationControls.dispose()
153
169
  layoutDiagnostics.dispose()
@@ -206,7 +222,8 @@ export function applyPrtsPlugin(ctx, environment) {
206
222
  }
207
223
  particleField.update(preferences)
208
224
  composerGlass.start()
209
- assistantGlass.start()
225
+ if (preferences.conversationStyle === 'deck-chat') assistantGlass.start()
226
+ else assistantGlass.dispose()
210
227
  toBottom.start()
211
228
  conversationControls.start()
212
229
  layoutDiagnostics.start()
@@ -222,9 +239,11 @@ export function applyPrtsPlugin(ctx, environment) {
222
239
  phase: result.persisted ? 'saved' : 'error',
223
240
  revision: settingsPersistence.revision + 1,
224
241
  }
242
+ const shouldPlayStartup = !safeMode && !wasEnabled && preferences.enabled && preferences.bootAnimation
243
+ if (shouldPlayStartup) particleField.setStartupActive(true)
225
244
  applyVisualState()
226
245
  sync()
227
- if (!safeMode && !wasEnabled && preferences.enabled && preferences.bootAnimation) startup.play()
246
+ if (shouldPlayStartup && !startup.play()) particleField.setStartupActive(false)
228
247
  }
229
248
 
230
249
 
@@ -297,13 +316,13 @@ export function applyPrtsPlugin(ctx, environment) {
297
316
  }
298
317
  ctx.effect(() => () => {
299
318
  statusSource.dispose()
319
+ particleField.dispose()
300
320
  startup.dispose({ restoreFocus: false })
301
321
  stopStartupReadyWatch()
302
322
  stopMountWatch()
303
323
  disposeOperations()
304
324
  composerGlass.dispose()
305
325
  assistantGlass.dispose()
306
- particleField.dispose()
307
326
  toBottom.dispose()
308
327
  conversationControls.dispose()
309
328
  layoutDiagnostics.dispose()
@@ -27,7 +27,10 @@ function markup(emblem) {
27
27
  <svg data-prts-scheme-icon="dark" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.2 15.4A8.4 8.4 0 0 1 8.6 3.8 8.5 8.5 0 1 0 20.2 15.4Z"></path></svg>
28
28
  <small>明暗</small>
29
29
  </button>
30
- <button type="button" data-prts-theme-disable aria-label="关闭 P.R.T.S. 主题"><b aria-hidden="true">×</b><small>关闭主题</small></button>
30
+ <button type="button" data-prts-theme-disable aria-label="退出 P.R.T.S.,返回 Harness 原生界面">
31
+ <svg data-prts-theme-disable-icon viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v8"></path><path d="M7.1 6.8a7 7 0 1 0 9.8 0"></path></svg>
32
+ <small>退出 P.R.T.S.</small>
33
+ </button>
31
34
  </div>
32
35
  </nav>
33
36
  ${themeSettingsMarkup()}
@@ -326,7 +326,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
326
326
  const rhodesIndex = Math.max(0, emblemSequence.findIndex(entry => entry.key === 'rhodes-island'))
327
327
  const suppliedHeroMask = normalizeEmblemMasks(heroEmblemMask ? [heroEmblemMask] : [], window).get('rhodes-island-hero')
328
328
  const empty = { mounted: false, hero: false, side: -1, transition: null, anchor: { horizontalProgress: 0, verticalProgress: 0 }, phase: 'error', error: '徽记资源异常' }
329
- if (!document || !window) return { update() {}, setSchemeTransitionActive() {}, inspect: () => empty, dispose() {} }
329
+ if (!document || !window) return { update() {}, setStartupActive() {}, setSchemeTransitionActive() {}, inspect: () => empty, dispose() {} }
330
330
 
331
331
  let preferences = {}
332
332
  let operation
@@ -396,6 +396,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
396
396
  let pauseStartedAt
397
397
  let hydrationPaused = false
398
398
  let schemeTransitionPaused = false
399
+ let startupPaused = false
399
400
  let hydrationTimer
400
401
  let hydrationFrame
401
402
  let hydrationStableFrames = 0
@@ -429,7 +430,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
429
430
  : preferences.conversationParticleDensity
430
431
  const densityProfile = density => PARTICLE_DENSITY_PROFILES[density] ?? PARTICLE_DENSITY_PROFILES.standard
431
432
  const particleRadius = density => densityProfile(density ?? activeDensity()).radius
432
- const canRenderFrame = () => !disposed && documentVisible && canvasVisible && !hydrationPaused && !schemeTransitionPaused && !resizing
433
+ const canRenderFrame = () => !disposed && documentVisible && canvasVisible && !hydrationPaused && !startupPaused && !schemeTransitionPaused && !resizing
433
434
  const layoutWidth = () => width
434
435
  const layoutHeight = () => height
435
436
  const layoutShiftX = () => 0
@@ -707,6 +708,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
707
708
  }
708
709
 
709
710
  function buildParticles() {
711
+ if (startupPaused) return false
710
712
  const selected = populationSource()
711
713
  if (!selected.length) {
712
714
  reportState({ phase: 'error', error: '徽记资源异常' })
@@ -1646,14 +1648,17 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1646
1648
  cancelFrame()
1647
1649
  return
1648
1650
  }
1649
- if (pauseStartedAt !== undefined) {
1650
- resumePausedTimelines(now)
1651
- if (canvas && particles.length) {
1652
- syncCanvasBackingStore()
1653
- cancelFrame()
1654
- draw(now)
1655
- return
1656
- }
1651
+ const resumedFromPause = pauseStartedAt !== undefined
1652
+ if (resumedFromPause) resumePausedTimelines(now)
1653
+ let particlesBuilt = false
1654
+ if (canvas && !particles.length && maskRecords.length) {
1655
+ particlesBuilt = buildParticles()
1656
+ }
1657
+ if (canvas && particles.length && (resumedFromPause || particlesBuilt)) {
1658
+ syncCanvasBackingStore()
1659
+ cancelFrame()
1660
+ draw(now)
1661
+ return
1657
1662
  }
1658
1663
  scheduleFrame()
1659
1664
  }
@@ -1819,6 +1824,12 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1819
1824
  if (!mediaQuery?.addEventListener) mediaQuery?.addListener?.(mediaListener)
1820
1825
  document.addEventListener?.('visibilitychange', onVisibilityChange)
1821
1826
  return {
1827
+ setStartupActive(active) {
1828
+ const next = Boolean(active)
1829
+ if (startupPaused === next) return
1830
+ startupPaused = next
1831
+ updateFrameGate()
1832
+ },
1822
1833
  setSchemeTransitionActive(active) {
1823
1834
  const next = Boolean(active)
1824
1835
  if (schemeTransitionPaused === next) return
@@ -1877,7 +1888,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1877
1888
  mutationObserver.observe(observationRoot, { childList: true, subtree: true })
1878
1889
  }
1879
1890
  }
1880
- if (ambient) ambient.hidden = preferences.texture === 'off'
1891
+ if (ambient) ambient.hidden = preferences.texture === 'off' && preferences.conversationStyle !== 'deck-chat'
1881
1892
  scheduleFrame()
1882
1893
  },
1883
1894
  inspect() {
@@ -1903,12 +1914,13 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1903
1914
  : transition?.toCount !== undefined
1904
1915
  ? Math.round(transition.fromCount + (transition.toCount - transition.fromCount) * easeInOut(transition.progress))
1905
1916
  : particles.length,
1906
- targetParticles: transition?.toCount ?? targetParticleCount(),
1917
+ targetParticles: startupPaused && !particles.length ? 0 : transition?.toCount ?? targetParticleCount(),
1907
1918
  density: activeDensity() || 'standard',
1908
1919
  firstFrameReady: canvas?.hasAttribute('data-prts-particle-ready') === true,
1909
1920
  hydrationPaused,
1910
1921
  sleeping: Boolean(context && particles.length && frame === undefined && canRenderFrame() && !motionReduced() && !phoneStatic()),
1911
1922
  schemeTransitionPaused,
1923
+ startupPaused,
1912
1924
  pointer: {
1913
1925
  active: pointerActive,
1914
1926
  clientX: pointerClientX,
@@ -6,7 +6,7 @@ export const PARTICLE_TRAVERSAL_SPEED_STEP = 0.25
6
6
  export const PARTICLE_TRAVERSAL_SPEED_DEFAULT = 1
7
7
 
8
8
  export const DEFAULT_PREFERENCES = Object.freeze({
9
- version: 8,
9
+ version: 9,
10
10
  enabled: false,
11
11
  preset: 'standard-tactical',
12
12
  texture: 'full',
@@ -18,6 +18,7 @@ export const DEFAULT_PREFERENCES = Object.freeze({
18
18
  particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
19
19
  particlePattern: 'orthogonal',
20
20
  railDefaultHidden: false,
21
+ conversationStyle: 'native',
21
22
  })
22
23
 
23
24
  export const VISUAL_PRESETS = Object.freeze({
@@ -59,6 +60,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
59
60
  material: Object.freeze(['glass']),
60
61
  accessibility: Object.freeze(['motion', 'bootAnimation']),
61
62
  navigation: Object.freeze(['railDefaultHidden']),
63
+ conversation: Object.freeze(['conversationStyle']),
62
64
  })
63
65
 
64
66
  const TEXTURES = new Set(['off', 'restrained', 'full'])
@@ -66,6 +68,7 @@ const GLASS_STRENGTHS = new Set(['off', 'soft', 'standard', 'clear'])
66
68
  const MOTIONS = new Set(['system', 'reduced'])
67
69
  const PARTICLE_DENSITIES = new Set(['sparse', 'light', 'standard', 'dense', 'ultra'])
68
70
  const PRESETS = new Set([...Object.keys(VISUAL_PRESETS), 'custom'])
71
+ const CONVERSATION_STYLES = new Set(['native', 'deck-chat'])
69
72
  const PRESET_LINKED_KEYS = new Set(Object.keys(VISUAL_PRESETS['standard-tactical']))
70
73
 
71
74
 
@@ -129,7 +132,7 @@ export function normalizePreferences(value) {
129
132
  const particlePreset = VISUAL_PRESETS[input.preset] ?? DEFAULT_PREFERENCES
130
133
  const legacyDensities = legacyParticleDensities(input)
131
134
  const normalized = {
132
- version: 8,
135
+ version: 9,
133
136
  enabled: booleanOr(input.enabled, DEFAULT_PREFERENCES.enabled),
134
137
  preset: enumOr(migratedPreset, PRESETS, DEFAULT_PREFERENCES.preset),
135
138
  texture: enumOr(input.texture, TEXTURES, DEFAULT_PREFERENCES.texture),
@@ -144,6 +147,7 @@ export function normalizePreferences(value) {
144
147
  ),
145
148
  particlePattern: 'orthogonal',
146
149
  railDefaultHidden: booleanOr(input.railDefaultHidden, DEFAULT_PREFERENCES.railDefaultHidden),
150
+ conversationStyle: enumOr(input.conversationStyle, CONVERSATION_STYLES, DEFAULT_PREFERENCES.conversationStyle),
147
151
  }
148
152
  normalized.preset = matchedVisualPreset(normalized)
149
153
  return normalized
@@ -190,7 +194,7 @@ export function loadPreferences(storage) {
190
194
  const raw = storage?.getItem(PRTS_STORAGE_KEY)
191
195
  if (raw === null || raw === undefined) return { ...DEFAULT_PREFERENCES }
192
196
  const parsed = JSON.parse(raw)
193
- if (![1, 2, 3, 4, 5, 6, 7, 8].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
197
+ if (![1, 2, 3, 4, 5, 6, 7, 8, 9].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
194
198
  return normalizePreferences(parsed)
195
199
  } catch {
196
200
  return { ...DEFAULT_PREFERENCES }
@@ -7,36 +7,73 @@ function closestCommonContainer(root, nodes) {
7
7
  return root && nodes.every(node => root.contains(node)) ? root : null
8
8
  }
9
9
 
10
+ const LAYOUT_CONTRACTS = [
11
+ {
12
+ version: '0.1.5',
13
+ center: '[data-slot="main"]',
14
+ auxiliary: '[data-slot="rightbar"]',
15
+ },
16
+ {
17
+ version: '0.1.2',
18
+ center: '[data-slot="conversation"]',
19
+ auxiliary: '[data-slot="details"]',
20
+ },
21
+ ]
22
+
10
23
  export function resolveRc7Regions(document, report = () => {}) {
11
24
  const root = document?.querySelector('[data-slot="root"]')
12
25
  const sidebarSlot = root?.querySelector('[data-slot="sidebar"]')
13
- const conversationSlot = root?.querySelector('[data-slot="conversation"]')
14
- const detailsSlot = root?.querySelector('[data-slot="details"]')
15
- if (!root || !sidebarSlot || !conversationSlot || !detailsSlot) {
26
+ const contract = LAYOUT_CONTRACTS
27
+ .map(candidate => ({
28
+ ...candidate,
29
+ centerSlot: root?.querySelector(candidate.center),
30
+ auxiliarySlot: root?.querySelector(candidate.auxiliary),
31
+ }))
32
+ .find(candidate => candidate.centerSlot && candidate.auxiliarySlot)
33
+ if (!root || !sidebarSlot || !contract) {
16
34
  report('required layout slots are unavailable', {
17
35
  root: Boolean(root), sidebar: Boolean(sidebarSlot),
18
- conversation: Boolean(conversationSlot), details: Boolean(detailsSlot),
36
+ main: Boolean(root?.querySelector('[data-slot="main"]')),
37
+ rightbar: Boolean(root?.querySelector('[data-slot="rightbar"]')),
38
+ conversation: Boolean(root?.querySelector('[data-slot="conversation"]')),
39
+ details: Boolean(root?.querySelector('[data-slot="details"]')),
19
40
  })
20
41
  return null
21
42
  }
43
+ const { centerSlot, auxiliarySlot } = contract
22
44
  const sidebar = sidebarSlot?.parentElement
23
- const center = conversationSlot?.parentElement
24
- const details = detailsSlot?.parentElement
45
+ const center = centerSlot?.parentElement
46
+ const auxiliary = auxiliarySlot?.parentElement
25
47
 
26
- if (!sidebar || !center || !details) {
48
+ if (!sidebar || !center || !auxiliary) {
27
49
  report('required layout region containers are unavailable', {
28
- sidebar: Boolean(sidebar), conversation: Boolean(center), details: Boolean(details),
50
+ sidebar: Boolean(sidebar), center: Boolean(center), auxiliary: Boolean(auxiliary),
29
51
  })
30
52
  return null
31
53
  }
32
54
 
33
- const frame = closestCommonContainer(root, [sidebar, center, details])
55
+ const frame = closestCommonContainer(root, [sidebar, center, auxiliary])
34
56
  if (!frame) {
35
57
  report('common layout container is unavailable')
36
58
  return null
37
59
  }
38
60
 
39
- return { root, frame, sidebar, center, details, sidebarSlot, conversationSlot, detailsSlot }
61
+ return {
62
+ root,
63
+ frame,
64
+ sidebar,
65
+ center,
66
+ auxiliary,
67
+ sidebarSlot,
68
+ centerSlot,
69
+ auxiliarySlot,
70
+ layoutVersion: contract.version,
71
+ // Legacy aliases keep the internal adapter face compatible with callers
72
+ // built against the 0.1.2 layout vocabulary.
73
+ conversationSlot: centerSlot,
74
+ details: auxiliary,
75
+ detailsSlot: auxiliarySlot,
76
+ }
40
77
  }
41
78
 
42
79
  export function createRc7Adapter({ document, warn } = {}) {
@@ -64,7 +101,7 @@ export function createRc7Adapter({ document, warn } = {}) {
64
101
  [regions.frame, 'frame'],
65
102
  [regions.sidebar, 'sessions'],
66
103
  [regions.center, 'operation'],
67
- [regions.details, 'details'],
104
+ [regions.auxiliary, 'auxiliary'],
68
105
  ]) {
69
106
  owned.push([node, node.getAttribute('data-prts-region')])
70
107
  node.setAttribute('data-prts-region', value)