@ash-qw/dsh-theme-prts 2.3.1 → 2.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-qw/dsh-theme-prts",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web and the official desktop client",
@@ -4,21 +4,22 @@ export function createBackgroundLayout({window,document}) {
4
4
  const root=document.documentElement
5
5
  const raf=window.requestAnimationFrame?.bind(window) ?? (fn=>window.setTimeout(fn,16))
6
6
  const caf=window.cancelAnimationFrame?.bind(window) ?? window.clearTimeout.bind(window)
7
- let state={}, frame, disposed=false, enabled=false, observing=false, operation, sessions
7
+ let state={}, frame, disposed=false, enabled=false, observing=false, hostFrame, operation, sessions
8
8
  const set=(name,value)=>{if(root.style.getPropertyValue(name)!==value) root.style.setProperty(name,value)}
9
9
  const update=()=>{
10
10
  frame=undefined
11
11
  if(disposed||!observing)return
12
+ const nextFrame=document.querySelector('[data-prts-region="frame"]')
12
13
  const nextOperation=document.querySelector('[data-prts-region="operation"]')
13
14
  const nextSessions=document.querySelector('[data-prts-region="sessions"]')
14
- if(operation!==nextOperation || sessions!==nextSessions){
15
- resize?.disconnect(); operation=nextOperation;sessions=nextSessions
16
- for(const node of [operation,sessions])if(node)resize?.observe(node)
15
+ if(hostFrame!==nextFrame || operation!==nextOperation || sessions!==nextSessions){
16
+ resize?.disconnect(); hostFrame=nextFrame;operation=nextOperation;sessions=nextSessions
17
+ for(const node of [hostFrame||operation,sessions])if(node)resize?.observe(node)
17
18
  }
18
19
  const railWidth=parseFloat(window.getComputedStyle(root).getPropertyValue('--prts-nav-width'))||0
19
20
  for(const slot of ['full','conversation','sidebar']){
20
21
  const entry=state[slot]
21
- const node=slot==='sidebar'?sessions:operation
22
+ const node=slot==='sidebar'?sessions:slot==='conversation'?(hostFrame||operation):operation
22
23
  const width=slot==='full'?window.innerWidth:(node?.clientWidth||0)+(slot==='sidebar'?railWidth:0)
23
24
  const height=slot==='full'?window.innerHeight:node?.clientHeight||0
24
25
  const active=entry?.region && width>0 && height>0
@@ -35,14 +36,19 @@ export function createBackgroundLayout({window,document}) {
35
36
  }
36
37
  }
37
38
  function schedule(){if(!disposed && observing && frame===undefined)frame=raf(update)}
38
- const resize=window.ResizeObserver?new window.ResizeObserver(schedule):null
39
+ // ResizeObserver runs after layout, before paint. Deferring again to rAF
40
+ // paints the new container with the previous image bounds for one frame.
41
+ const resize=window.ResizeObserver?new window.ResizeObserver(()=>{
42
+ if(frame!==undefined)caf(frame)
43
+ update()
44
+ }):null
39
45
  const mutations=new window.MutationObserver(()=>{
40
46
  if(!operation?.isConnected || !sessions?.isConnected)schedule()
41
47
  })
42
48
  const attributes=new window.MutationObserver(schedule)
43
49
  function clear(){
44
50
  caf(frame);frame=undefined;resize?.disconnect();mutations.disconnect();attributes.disconnect();window.removeEventListener('resize',schedule)
45
- operation=undefined;sessions=undefined;observing=false
51
+ hostFrame=undefined;operation=undefined;sessions=undefined;observing=false
46
52
  for(const slot of ['full','conversation','sidebar'])for(const axis of ['x','y','width'])root.style.removeProperty(`--prts-protected-${slot}-${axis}`)
47
53
  }
48
54
  function reconcile(){
@@ -2,7 +2,7 @@ import { createBackgroundLayout } from './background-layout.js'
2
2
  import { mountBackgroundRegionEditor, backgroundRegionMarkup } from './background-region-editor.js'
3
3
  import { opacityDialMarkup, mountOpacityDials } from './opacity-dial.js'
4
4
  const BACKGROUND_ENDPOINT = '/prts-theme-api/backgrounds'
5
- const backgroundLabels = { sidebar: '侧边栏', conversation: '会话正文', full: '完整背景' }
5
+ const backgroundLabels = { sidebar: '左侧边栏', conversation: '会话正文', full: '完整背景' }
6
6
  function backgroundImageUrl(entry) {
7
7
  return /^[a-f0-9-]{36}$/.test(entry?.id ?? '') ? `${BACKGROUND_ENDPOINT}/image/${entry.id}` : null
8
8
  }
@@ -77,7 +77,7 @@ export function backgroundMarkup(heroContrastControl = '') {
77
77
  return `<section data-prts-background-settings>
78
78
  <header><strong>会话背景</strong><small>分区使用不同图片,或让一张完整背景贯穿侧边栏与正文。</small></header>
79
79
  <div data-prts-background-modes role="group" aria-label="背景模式">
80
- ${[['off','默认背景','使用主题原有底色'],['split','分区背景','侧栏与正文独立配置'],['full','完整背景','一张图片贯穿整个界面']].map(([value,label,hint]) => `<button type="button" data-prts-setting-key="backgroundMode" data-prts-setting-value="${value}" aria-pressed="false"><span data-prts-background-diagram="${value}" aria-hidden="true"><i></i><i></i></span><span><strong>${label}</strong><small>${hint}</small></span></button>`).join('')}
80
+ ${[['off','默认背景','使用主题原有底色'],['split','分区背景','左侧边栏与正文独立配置'],['full','完整背景','一张图片贯穿整个界面']].map(([value,label,hint]) => `<button type="button" data-prts-setting-key="backgroundMode" data-prts-setting-value="${value}" aria-pressed="false"><span data-prts-background-diagram="${value}" aria-hidden="true"><i></i><i></i></span><span><strong>${label}</strong><small>${hint}</small></span></button>`).join('')}
81
81
  </div>
82
82
  <div data-prts-background-images hidden>${Object.entries(backgroundLabels).map(([slot,label]) => `<article data-prts-background-card="${slot}" hidden>
83
83
  <div data-prts-background-card-heading><strong>${label}</strong><button type="button" data-prts-background-remove="${slot}" aria-label="移除${label}图片" hidden>移除</button></div>
@@ -78,7 +78,12 @@ export function createHostGeometryAdapter({ document, window }) {
78
78
  shouldMeasure = true
79
79
  }
80
80
  }
81
- if (shouldMeasure || !entries.length) scheduleMeasure()
81
+ // Keep shared wallpaper geometry in the same paint as the resized sidebar.
82
+ if (shouldMeasure || !entries.length) {
83
+ if (frameRequest !== undefined) window?.cancelAnimationFrame?.(frameRequest)
84
+ frameRequest = undefined
85
+ measure()
86
+ }
82
87
  }
83
88
 
84
89
  function stopObservation() {
@@ -28,6 +28,13 @@ export function createNativePreferences({ storage, onChange }) {
28
28
  migrationAttempted = true
29
29
  pending = { ...legacy, ...pending }
30
30
  }
31
+ // Materialize the old shared sidebar values once, so later left-side edits
32
+ // cannot change a right-side preference that was never written by the Host.
33
+ if (s.value?.preferencesInitialized && writable(s)) {
34
+ for (const key of ['rightSidebarOpacity', 'rightSidebarBlur']) {
35
+ if (!Object.hasOwn(s.value, key) && !Object.hasOwn(pending, key) && !Object.hasOwn(inFlight, key) && !Object.hasOwn(failed, key)) pending[key] = host[key]
36
+ }
37
+ }
31
38
  current = normalizePreferences({ ...host, ...inFlight, ...pending })
32
39
  if (s.value?.preferencesInitialized) {
33
40
  legacy = null
@@ -1,7 +1,8 @@
1
1
  // Ring geometry adapted from React Bits CometDial (David Haz).
2
2
  // See src/vendor/react-bits/LICENSE.md and THIRD_PARTY_NOTICES.md.
3
3
  export const BACKGROUND_OPACITY_CONTROLS = Object.freeze([
4
- { key: 'sidebarOpacity', label: '会话列表侧栏', property: '--prts-sidebar-opacity', value: 94, blurKey: 'sidebarBlur', blurProperty: '--prts-sidebar-blur', blurDefault: 8 },
4
+ { key: 'sidebarOpacity', label: '左侧边栏', property: '--prts-sidebar-opacity', value: 94, blurKey: 'sidebarBlur', blurProperty: '--prts-sidebar-blur', blurDefault: 8 },
5
+ { key: 'rightSidebarOpacity', label: '右侧边栏', property: '--prts-right-sidebar-opacity', value: 94, blurKey: 'rightSidebarBlur', blurProperty: '--prts-right-sidebar-blur', blurDefault: 8 },
5
6
  { key: 'railOpacity', label: '设置侧栏', property: '--prts-rail-opacity', value: 70, blurKey: 'railBlur', blurProperty: '--prts-rail-blur', blurDefault: 12 },
6
7
  { key: 'bakerOpacity', label: 'Baker 气泡', property: '--prts-baker-opacity', value: 50, blurKey: 'bakerBlur', blurProperty: '--prts-baker-blur', blurDefault: 8, theme: 'deck-chat' },
7
8
  { key: 'classicUserOpacity', label: '经典用户消息', property: '--prts-classic-user-opacity', value: 50, blurKey: 'classicUserBlur', blurProperty: '--prts-classic-user-blur', blurDefault: 8, theme: 'native' },
@@ -16,7 +17,8 @@ export function opacityDialMarkup() {
16
17
  const controls = BACKGROUND_OPACITY_CONTROLS.map(({key,label,value,blurDefault}) => {
17
18
  const sample = key === 'bakerOpacity' ? '<span>这是一条回复</span><i></i>'
18
19
  : key === 'classicUserOpacity' ? '<span>这是我发送的消息</span><i></i>'
19
- : key === 'railOpacity' ? '<b>◇</b><b>☼</b><b>⚙</b>' : '<span>工作区</span><i></i><i></i>'
20
+ : key === 'railOpacity' ? '<b>◇</b><b>☼</b><b>⚙</b>'
21
+ : key === 'rightSidebarOpacity' ? '<span>侧边栏</span><i></i><i></i>' : '<span>工作区</span><i></i><i></i>'
20
22
  return `<div data-prts-opacity-control="${key}">
21
23
  <div data-prts-opacity-heading><strong>${label}</strong><button type="button" data-prts-opacity-reset data-prts-opacity-reset-tooltip="恢复到 ${value}%" aria-label="恢复${label}透明度到 ${value}%"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 10a7.5 7.5 0 1 1 .7 5.2M4.5 4.5V10H10"/></svg></button></div>
22
24
  <div data-prts-opacity-dial><svg viewBox="0 0 200 200" role="slider" tabindex="0" aria-label="${label}透明度" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${value}" aria-valuetext="${value}%"><circle data-dial-focus cx="100" cy="100" r="97"/><path data-dial-track d="${dialArc(110,430)}"/><path data-dial-lit/><path data-dial-tail/><circle data-dial-head r="8"/></svg><label data-prts-opacity-number><input type="number" min="0" max="100" step="1" value="${value}" aria-label="${label}透明度百分比"><span>%</span></label></div>
@@ -24,8 +26,8 @@ export function opacityDialMarkup() {
24
26
  <div data-prts-blur-control hidden><label><span>模糊</span><input type="range" min="0" max="32" step="1" value="${blurDefault}" aria-label="${label}模糊度"></label><output>${blurDefault}px</output><button type="button" data-prts-blur-reset title="恢复到 ${blurDefault}px" aria-label="恢复${label}模糊度到 ${blurDefault}px"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 10a7.5 7.5 0 1 1 .7 5.2M4.5 4.5V10H10"/></svg></button></div>
25
27
  <small data-prts-opacity-note></small>
26
28
  </div>`
27
- }).join('')
28
- return `<fieldset data-prts-opacity-settings hidden><legend>材质透明度与模糊度</legend><small>透明度数值越高越透明;模糊度以像素计。调整时预览,松手后保存。</small><div data-prts-opacity-grid>${controls}</div></fieldset>`
29
+ })
30
+ return `<fieldset data-prts-opacity-settings hidden><legend>材质透明度与模糊度</legend><small>透明度数值越高越透明;模糊度以像素计。调整时预览,松手后保存。</small><div data-prts-opacity-grid>${controls.join('')}</div></fieldset>`
29
31
  }
30
32
  export function mountOpacityDials({ section, window, onChange }) {
31
33
  const root = section.ownerDocument.documentElement
@@ -108,14 +110,14 @@ export function mountOpacityDials({ section, window, onChange }) {
108
110
  for(const [el,type,fn] of bindings)el.addEventListener(type,fn)
109
111
  paint(value)
110
112
  return {
111
- update(preferences, active) {
113
+ update(preferences, active, inactiveNote) {
112
114
  container.hidden = Boolean(definition.theme && preferences.conversationStyle !== definition.theme)
113
115
  saved = 100 - Math.round((preferences[definition.key] ?? (100 - definition.value) / 100) * 100)
114
116
  if (!grip && section.ownerDocument.activeElement !== input) paint(saved)
115
117
  savedBlur = preferences[definition.blurKey] ?? definition.blurDefault
116
118
  if (section.ownerDocument.activeElement !== blurInput) paintBlur(savedBlur)
117
119
  blurControl.hidden = !active
118
- container.querySelector('[data-prts-opacity-note]').textContent = active ? '' : '当前未启用此材质'
120
+ container.querySelector('[data-prts-opacity-note]').textContent = active ? '' : inactiveNote
119
121
  },
120
122
  dispose() { if(grip)finish({},true);window.clearTimeout(timer);for(const [el,type,fn] of bindings)el.removeEventListener(type,fn) },
121
123
  }
@@ -127,10 +129,13 @@ export function mountOpacityDials({ section, window, onChange }) {
127
129
  section.querySelector('[data-prts-opacity-settings]').hidden = preferences.backgroundMode === 'off'
128
130
  const conversationImage = full || (split && Boolean(state.conversation))
129
131
  const sidebarImage = full || (split && Boolean(state.sidebar))
130
- const active = [sidebarImage, sidebarImage,
132
+ const active = [sidebarImage || conversationImage, conversationImage, sidebarImage,
131
133
  conversationImage && preferences.conversationStyle === 'deck-chat',
132
134
  conversationImage && preferences.conversationStyle === 'native']
133
- controls.forEach((control,i)=>control.update(preferences,active[i]))
135
+ const missingImage = full || !split ? '上传完整背景图片后生效' : null
136
+ const inactiveNotes = [missingImage || '上传左侧边栏或正文图片后生效', missingImage || '上传正文图片后生效',
137
+ missingImage || '上传左侧边栏图片后生效', missingImage || '上传正文图片后生效', missingImage || '上传正文图片后生效']
138
+ controls.forEach((control,i)=>control.update(preferences,active[i],inactiveNotes[i]))
134
139
  },
135
140
  dispose() {for(const control of controls)control.dispose()},
136
141
  }
@@ -29,10 +29,12 @@ export const DEFAULT_PREFERENCES = Object.freeze({
29
29
  backgroundDim: .45,
30
30
  heroContrast: true,
31
31
  sidebarOpacity: .06,
32
+ rightSidebarOpacity: .06,
32
33
  railOpacity: .3,
33
34
  bakerOpacity: .5,
34
35
  classicUserOpacity: .5,
35
36
  sidebarBlur: 8,
37
+ rightSidebarBlur: 8,
36
38
  railBlur: 12,
37
39
  bakerBlur: 8,
38
40
  classicUserBlur: 8,
@@ -100,7 +102,7 @@ export const PARTICLE_DETAIL_LEVELS = Object.freeze({
100
102
  precise: Object.freeze({ conversationParticleDensity: 'dense', heroParticleDensity: 'ultra' }),
101
103
  })
102
104
  export const PREFERENCE_GROUPS = Object.freeze({
103
- background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'heroContrast', 'sidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
105
+ background: Object.freeze(['texture', 'backgroundMode', 'backgroundDim', 'heroContrast', 'sidebarOpacity', 'rightSidebarOpacity', 'railOpacity', 'bakerOpacity', 'classicUserOpacity', 'sidebarBlur', 'rightSidebarBlur', 'railBlur', 'bakerBlur', 'classicUserBlur']),
104
106
  particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
105
107
  material: Object.freeze(['glass', 'composerGlass']),
106
108
  accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
@@ -214,10 +216,12 @@ export function normalizePreferences(value) {
214
216
  bakerLoading: input.bakerLoading === true,
215
217
  backgroundMode: ['off', 'split', 'full'].includes(input.backgroundMode) ? input.backgroundMode : 'off',
216
218
  sidebarOpacity: typeof input.sidebarOpacity === 'number' && Number.isFinite(input.sidebarOpacity) ? Math.min(1, Math.max(0, input.sidebarOpacity)) : .06,
219
+ rightSidebarOpacity: typeof input.rightSidebarOpacity === 'number' && Number.isFinite(input.rightSidebarOpacity) ? Math.min(1, Math.max(0, input.rightSidebarOpacity)) : typeof input.sidebarOpacity === 'number' && Number.isFinite(input.sidebarOpacity) ? Math.min(1, Math.max(0, input.sidebarOpacity)) : .06,
217
220
  railOpacity: typeof input.railOpacity === 'number' && Number.isFinite(input.railOpacity) ? Math.min(1, Math.max(0, input.railOpacity)) : .3,
218
221
  bakerOpacity: typeof input.bakerOpacity === 'number' && Number.isFinite(input.bakerOpacity) ? Math.min(1, Math.max(0, input.bakerOpacity)) : .5,
219
222
  classicUserOpacity: typeof input.classicUserOpacity === 'number' && Number.isFinite(input.classicUserOpacity) ? Math.min(1, Math.max(0, input.classicUserOpacity)) : .5,
220
223
  sidebarBlur: materialBlurOr(input.sidebarBlur, DEFAULT_PREFERENCES.sidebarBlur),
224
+ rightSidebarBlur: materialBlurOr(input.rightSidebarBlur, materialBlurOr(input.sidebarBlur, DEFAULT_PREFERENCES.sidebarBlur)),
221
225
  railBlur: materialBlurOr(input.railBlur, DEFAULT_PREFERENCES.railBlur),
222
226
  bakerBlur: materialBlurOr(input.bakerBlur, DEFAULT_PREFERENCES.bakerBlur),
223
227
  classicUserBlur: materialBlurOr(input.classicUserBlur, DEFAULT_PREFERENCES.classicUserBlur),
@@ -771,8 +771,10 @@ export function createThemeController({ document, window, cssText, service, curs
771
771
  root.dataset.prtsBackgroundMode = preferences.backgroundMode
772
772
  root.style.setProperty('--prts-background-shade', `${preferences.backgroundDim * 100}%`)
773
773
  for (const name of ['sidebar', 'rail', 'baker']) root.style.setProperty(`--prts-${name}-opacity`, `${preferences[`${name}Opacity`] * 100}%`)
774
+ root.style.setProperty('--prts-right-sidebar-opacity', `${preferences.rightSidebarOpacity * 100}%`)
774
775
  root.style.setProperty('--prts-classic-user-opacity', `${preferences.classicUserOpacity * 100}%`)
775
776
  for (const name of ['sidebar', 'rail', 'baker']) root.style.setProperty(`--prts-${name}-blur`, `${preferences[`${name}Blur`]}px`)
777
+ root.style.setProperty('--prts-right-sidebar-blur', `${preferences.rightSidebarBlur}px`)
776
778
  root.style.setProperty('--prts-classic-user-blur', `${preferences.classicUserBlur}px`)
777
779
  refresh()
778
780
  },
@@ -1,4 +1,4 @@
1
- import { THEME_PACKAGE_LIMIT, validateThemePackage, describeThemePackage } from './theme-transfer.js'
1
+ import { THEME_PACKAGE_LIMIT, validateThemePackage, describeThemePackage, ignoredThemePackageFields } from './theme-transfer.js'
2
2
 
3
3
  export function themeTransferMarkup() {
4
4
  return `<dialog data-prts-transfer-dialog aria-labelledby="prts-transfer-title">
@@ -20,6 +20,7 @@ export function themeTransferMarkup() {
20
20
  </div>
21
21
  <div data-prts-transfer-preview hidden>
22
22
  <strong>确认导入</strong><p data-prts-transfer-summary></p>
23
+ <p data-prts-transfer-compatibility hidden role="alert"></p>
23
24
  <small>应用前会下载当前配置备份,请保留备份文件。</small>
24
25
  <div data-prts-transfer-buttons><button type="button" data-prts-transfer-apply>确认导入</button><button type="button" data-prts-transfer-cancel>取消</button></div>
25
26
  </div>
@@ -43,6 +44,7 @@ export function mountThemeTransfer({ panel, window, transfer }) {
43
44
  const render = () => {
44
45
  if (disposed) return
45
46
  get('preview').hidden = !pending
47
+ if (!pending) get('compatibility').hidden = true
46
48
  get('recovery').hidden = !transfer.hasBackup
47
49
  for (const control of section.querySelectorAll('button,input')) control.disabled = busy
48
50
  // Prevent local editors from racing a multi-service import; other clients use revision checks.
@@ -72,6 +74,12 @@ export function mountThemeTransfer({ panel, window, transfer }) {
72
74
  if (disposed || selected !== generation) return ''
73
75
  pending = value
74
76
  get('summary').textContent = describeThemePackage(value)
77
+ const ignored = ignoredThemePackageFields(value)
78
+ const compatibility = get('compatibility')
79
+ compatibility.hidden = ignored.length === 0
80
+ compatibility.textContent = ignored.length
81
+ ? `当前版本不支持以下配置,导入后将丢失:${ignored.join('、')}。请保留原文件。`
82
+ : ''
75
83
  return '已读取文件,请确认导入内容。'
76
84
  })
77
85
  }
@@ -7,6 +7,33 @@ const transferRoles = ['user', 'assistant']
7
7
  const transferBuiltins = ['rhodes-island', 'lungmen', 'penguin-logistics', 'rhine-lab', 'reunion']
8
8
  const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
9
9
  const encodedImage = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= Math.ceil(limit / 3) * 4 && value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
10
+ const unknownFields = (value, known, prefix) => record(value)
11
+ ? Object.keys(value).filter(key => !known.includes(key)).map(key => `${prefix}.${key}`)
12
+ : []
13
+
14
+ export function ignoredThemePackageFields(value) {
15
+ const ignored = unknownFields(value.preferences, Object.keys(DEFAULT_PREFERENCES), '设置')
16
+ const assets = value.assets
17
+ if (!assets) return ignored
18
+ ignored.push(...unknownFields(assets, ['backgrounds', 'backgroundRegions', 'avatars', 'emblems'], '素材'))
19
+ ignored.push(...unknownFields(assets.backgrounds, transferSlots, '背景'))
20
+ ignored.push(...unknownFields(assets.backgroundRegions, transferSlots, '背景保护区域'))
21
+ for (const slot of transferSlots) ignored.push(...unknownFields(assets.backgroundRegions?.[slot], ['x', 'y', 'width', 'height'], `背景保护区域.${slot}`))
22
+ ignored.push(...unknownFields(assets.avatars, transferRoles, '头像'))
23
+ for (const role of transferRoles) {
24
+ ignored.push(...unknownFields(assets.avatars?.[role], ['image', 'crop'], `头像.${role}`))
25
+ ignored.push(...unknownFields(assets.avatars?.[role]?.crop, ['zoom', 'x', 'y'], `头像.${role}.crop`))
26
+ }
27
+ const emblems = assets.emblems
28
+ ignored.push(...unknownFields(emblems, ['items', 'hero', 'conversation'], '徽记'))
29
+ for (const [index, item] of (emblems?.items ?? []).entries()) {
30
+ ignored.push(...unknownFields(item, ['format', 'version', 'label', 'params', 'image'], `徽记.items[${index}]`))
31
+ ignored.push(...unknownFields(item?.params, ['mode', 'lineSource', 'cleanup', 'threshold', 'zoom', 'x', 'y'], `徽记.items[${index}].params`))
32
+ }
33
+ ignored.push(...unknownFields(emblems?.hero, ['kind', 'key', 'label', 'asset'], '徽记.hero'))
34
+ for (const [index, entry] of (emblems?.conversation ?? []).entries()) ignored.push(...unknownFields(entry, ['kind', 'key', 'label', 'asset'], `徽记.conversation[${index}]`))
35
+ return ignored
36
+ }
10
37
 
11
38
  export function validateThemePackage(value) {
12
39
  if (!record(value) || value.format !== 'prts-theme' || value.version !== 1) throw Error('不支持的主题文件格式或版本')
@@ -11,8 +11,27 @@ html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-side
11
11
  & [data-prts-region="sessions"]{background-position:center,calc(var(--prts-wall-x) - var(--prts-nav-width)) var(--prts-protected-sidebar-y, 50%)}
12
12
  }
13
13
  html[data-dsh-prts][data-prts-rail-mode="overlay"]:is([data-prts-background-mode="split"][data-prts-background-sidebar],[data-prts-background-mode="full"][data-prts-background-full]) [data-prts-nav-rail] { background: transparent; }
14
- html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-conversation] [data-prts-region="operation"] {
15
- background: linear-gradient(var(--prts-background-wash), var(--prts-background-wash)), var(--prts-background-conversation) var(--prts-protected-conversation-x, 50%) var(--prts-protected-conversation-y, 50%) / cover no-repeat;
14
+ html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-conversation] {
15
+ & [data-prts-region="frame"] { background: linear-gradient(var(--prts-background-wash), var(--prts-background-wash)), var(--prts-background-conversation) var(--prts-protected-conversation-x, 50%) var(--prts-protected-conversation-y, 50%) / cover no-repeat; }
16
+ & [data-prts-region="operation"] { background: transparent; }
17
+ &:not([data-prts-background-sidebar]) [data-prts-region="sessions"] { background: transparent; }
18
+ }
19
+ /* The native right dock paints an opaque tab host over the frame wallpaper. */
20
+ html[data-dsh-prts]:is([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"][data-prts-background-conversation]) [data-prts-region="auxiliary"] { background: transparent; }
21
+ html[data-dsh-prts]:is([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"][data-prts-background-conversation]) [data-prts-region="auxiliary"] :is([data-dockkit-host="dock"] > :first-child,[data-dockkit-empty]) {
22
+ position: relative;
23
+ isolation: isolate;
24
+ background: transparent;
25
+ }
26
+ html[data-dsh-prts]:is([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"][data-prts-background-conversation]) [data-prts-region="auxiliary"] :is([data-dockkit-host="dock"] > :first-child,[data-dockkit-empty])::before {
27
+ content: "";
28
+ position: absolute;
29
+ inset: 0;
30
+ z-index: -1;
31
+ pointer-events: none;
32
+ background: color-mix(in srgb, var(--prts-panel) var(--prts-right-sidebar-opacity, 6%), transparent);
33
+ -webkit-backdrop-filter: blur(var(--prts-right-sidebar-blur, 8px)) saturate(1.15);
34
+ backdrop-filter: blur(var(--prts-right-sidebar-blur, 8px)) saturate(1.15);
16
35
  }
17
36
  html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] [data-prts-ambient-layer],
18
37
  html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-conversation] [data-prts-ambient-layer] { display: none !important; }
@@ -74,8 +93,7 @@ html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full]
74
93
  html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-sidebar] [data-prts-region="sessions"] [data-slot="sidebar.workspaces"] span[class$="_fade"] { display: none !important; }
75
94
 
76
95
  /* Filter the plate, not the ancestor of fixed controls. */
77
- html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] [data-prts-region="sessions"]::before,
78
- html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-sidebar] [data-prts-region="sessions"]::before {
96
+ html[data-dsh-prts]:is([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"]:is([data-prts-background-sidebar],[data-prts-background-conversation])) [data-prts-region="sessions"]::before {
79
97
  content: "";
80
98
  position: absolute;
81
99
  inset: 0;
@@ -87,8 +105,7 @@ backdrop-filter: blur(var(--prts-sidebar-blur, 8px)) saturate(1.15);
87
105
  box-shadow: inset -1px 0 color-mix(in srgb, var(--prts-ink) 10%, transparent), inset 0 1px rgb(255 255 255 / 8%);
88
106
  }
89
107
  /* Raise sidebar controls above glass. */
90
- html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] [data-prts-region="sessions"] [data-slot="sidebar"] > :first-child,
91
- html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-sidebar] [data-prts-region="sessions"] [data-slot="sidebar"] > :first-child {
108
+ html[data-dsh-prts]:is([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"]:is([data-prts-background-sidebar],[data-prts-background-conversation])) [data-prts-region="sessions"] [data-slot="sidebar"] > :first-child {
92
109
  position: relative;
93
110
  z-index: 1;
94
111
  }
@@ -273,13 +290,16 @@ border-radius: 6px;
273
290
  background: var(--prts-background-full, linear-gradient(135deg, #8bb6c2, #c7b69c)) center / cover;
274
291
  --prts-preview-surface: var(--prts-panel);
275
292
  }
276
- html[data-prts-background-mode="split"] :is([data-prts-material-preview="sidebarOpacity"],[data-prts-material-preview="railOpacity"]) { background-image: var(--prts-background-sidebar, linear-gradient(135deg, #8bb6c2, #c7b69c)); }
293
+ html[data-prts-background-mode="split"] [data-prts-material-preview="sidebarOpacity"] { background-image: var(--prts-background-sidebar, var(--prts-background-conversation, linear-gradient(135deg, #8bb6c2, #c7b69c))); }
294
+ html[data-prts-background-mode="split"] [data-prts-material-preview="rightSidebarOpacity"] { background-image: var(--prts-background-conversation, linear-gradient(135deg, #8bb6c2, #c7b69c)); }
295
+ html[data-prts-background-mode="split"] [data-prts-material-preview="railOpacity"] { background-image: var(--prts-background-sidebar, linear-gradient(135deg, #8bb6c2, #c7b69c)); }
277
296
  html[data-prts-background-mode="split"] [data-prts-material-preview="bakerOpacity"] { background-image: var(--prts-background-conversation, linear-gradient(135deg, #8bb6c2, #c7b69c)); }
278
297
  html[data-prts-background-mode="split"] [data-prts-material-preview="classicUserOpacity"] { background-image: var(--prts-background-conversation, linear-gradient(135deg, #8bb6c2, #c7b69c)); }
279
298
  [data-prts-material-preview]::before { content: ""; position: absolute; inset: 0; background: var(--prts-background-wash); }
280
299
  [data-prts-material-preview-surface] { position: absolute; inset: 0; display: grid; align-content: center; gap: 6px; padding: 10px; font-size: 10px; text-align: left; color: var(--prts-ink); background: color-mix(in srgb, var(--prts-preview-surface) var(--prts-sidebar-opacity, 6%), transparent); backdrop-filter: blur(var(--prts-sidebar-blur, 8px)) saturate(1.15); }
281
300
  [data-prts-material-preview-surface] i { display: block; height: 8px; border-radius: 2px; background: color-mix(in srgb, var(--prts-ink) 18%, transparent); }
282
301
  [data-prts-material-preview-surface] i:last-child { width: 65%; }
302
+ [data-prts-material-preview="rightSidebarOpacity"] [data-prts-material-preview-surface] { background: color-mix(in srgb, var(--prts-preview-surface) var(--prts-right-sidebar-opacity, 6%), transparent); backdrop-filter: blur(var(--prts-right-sidebar-blur, 8px)) saturate(1.15); }
283
303
  [data-prts-material-preview="railOpacity"] [data-prts-material-preview-surface] { inset: 0 auto 0 0; width: 40%; padding: 5px; justify-items: center; gap: 2px; background: color-mix(in srgb, var(--prts-preview-surface) var(--prts-rail-opacity, 30%), transparent); backdrop-filter: blur(var(--prts-rail-blur, 12px)) saturate(1.15); }
284
304
  [data-prts-material-preview="bakerOpacity"] [data-prts-material-preview-surface] { inset: 16px 10px; border-radius: 0 10px 10px 10px; background: rgb(69 70 68 / var(--prts-baker-opacity, 50%)); backdrop-filter: blur(var(--prts-baker-blur, 8px)) saturate(1.08); }
285
305
  html[data-prts-scheme="light"] [data-prts-material-preview="bakerOpacity"] [data-prts-material-preview-surface] { background: rgb(223 227 223 / var(--prts-baker-opacity, 50%)); }
@@ -970,6 +970,7 @@ html[data-dsh-prts] [data-prts-transfer] {
970
970
  & [data-prts-transfer-file-hint] { display:grid; place-items:center; flex:1; gap:6px; padding:12px; border:1px dashed var(--prts-line-strong); }
971
971
  & [data-prts-transfer-file-hint] > span { font-size:24px; color:var(--prts-muted); }
972
972
  & [data-prts-transfer-buttons] { display:flex; gap:8px; }
973
+ & [data-prts-transfer-compatibility] { margin:0; padding:10px 12px; border:1px solid var(--prts-yellow); overflow-wrap:anywhere; }
973
974
  & [data-prts-transfer-recovery] { border:0; border-top:1px solid var(--prts-line); padding:16px 0 0; }
974
975
  & [data-prts-transfer-status]:empty { display:none; }
975
976
  }