@ash-qw/dsh-theme-prts 2.0.0 → 2.1.1

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.
@@ -1,3 +1,4 @@
1
+ import { backgroundMarkup, mountBackgroundSettings } from './custom-background.js'
1
2
  import { avatarMarkup, mountAvatarSettings } from './custom-avatar.js'
2
3
  import { customEmblemMarkup, mountCustomEmblemEditor } from './custom-emblem.js'
3
4
  import { PARTICLE_TRAVERSAL_SPEED_DEFAULT, PARTICLE_TRAVERSAL_SPEED_MAX, PARTICLE_TRAVERSAL_SPEED_MIN, PARTICLE_TRAVERSAL_SPEED_STEP, SESSION_FLOW_SPEED_DEFAULT, SESSION_FLOW_SPEED_MAX, SESSION_FLOW_SPEED_MIN, SESSION_FLOW_SPEED_STEP, resolveParticleDetail } from './preferences.js'
@@ -37,7 +38,11 @@ function previewSetting(key, label, description) {
37
38
  }
38
39
 
39
40
  function settingButtons(key, label, options) {
40
- const note = key === 'motion'
41
+ const note = key === 'composerGlass'
42
+ ? '<small data-prts-setting-note>增强输入框的背景模糊与轻微折射,关闭时使用原有材质</small>'
43
+ : key === 'bakerLoading'
44
+ ? '<small data-prts-setting-note>每轮正文生成时显示三点动画,完成后显示全文;中断时保留已收到的内容</small>'
45
+ : key === 'motion'
41
46
  ? '<small data-prts-setting-note>关闭后减少装饰动效,保留运行状态等必要反馈</small><small data-prts-setting-note data-prts-effective-motion hidden>系统当前要求减少动态</small>'
42
47
  : key === 'railDefaultHidden'
43
48
  ? '<small data-prts-setting-note>在任意窗口宽度下默认收起 P.R.T.S. 导航,可从左侧按钮临时展开</small>'
@@ -54,7 +59,7 @@ function settingButtons(key, label, options) {
54
59
  : key === 'sessionFlowPalette'
55
60
  ? '<small data-prts-setting-note>同时适配日间与夜间模式</small>'
56
61
  : ''
57
- if (['motion', 'cursor', 'railDefaultHidden', 'sessionFlow', 'sessionGlow', 'bakerGroupAvatars', 'conversationEmblem', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning'].includes(key)) return `<div data-prts-setting-row="${key}">
62
+ if (['composerGlass', 'bakerLoading', 'motion', 'cursor', 'railDefaultHidden', 'sessionFlow', 'sessionGlow', 'bakerGroupAvatars', 'conversationEmblem', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning'].includes(key)) return `<div data-prts-setting-row="${key}">
58
63
  <span><strong>${label}</strong>${note}</span>
59
64
  <button type="button" role="switch" aria-label="${label}" aria-checked="false" data-prts-setting-switch="${key}" ${key === 'cursor' ? 'data-prts-cursor-switch' : ''}><span aria-hidden="true"></span></button>
60
65
  </div>`
@@ -116,7 +121,8 @@ export function themeSettingsMarkup() {
116
121
  <div data-prts-settings-body>
117
122
  <div data-prts-settings-sidebar>
118
123
  <nav data-prts-settings-nav role="tablist" aria-label="设置分区">
119
- <button type="button" data-prts-settings-jump="appearance" role="tab" id="prts-tab-appearance" aria-controls="prts-page-appearance" aria-selected="true" tabindex="0">会话外观</button>
124
+ <button type="button" data-prts-settings-jump="appearance" role="tab" id="prts-tab-appearance" aria-controls="prts-page-appearance" aria-selected="true" tabindex="0">会话主题</button>
125
+ <button type="button" data-prts-settings-jump="background" role="tab" id="prts-tab-background" aria-controls="prts-page-background" aria-selected="false" tabindex="-1">会话背景</button>
120
126
  <button type="button" data-prts-settings-jump="interaction" role="tab" id="prts-tab-interaction" aria-controls="prts-page-interaction" aria-selected="false" tabindex="-1">交互</button>
121
127
  <button type="button" data-prts-settings-jump="flow" role="tab" id="prts-tab-flow" aria-controls="prts-page-flow" aria-selected="false" tabindex="-1">会话流线</button>
122
128
  <button type="button" data-prts-settings-jump="particles" role="tab" id="prts-tab-particles" aria-controls="prts-page-particles" aria-selected="false" tabindex="-1">粒子徽记</button>
@@ -124,7 +130,7 @@ export function themeSettingsMarkup() {
124
130
  </nav>
125
131
  <footer data-prts-settings-actions aria-label="全局主题设置操作">
126
132
  <div data-prts-persistence-error hidden role="alert">
127
- <span><strong>仅本次会话生效</strong><small>无法写入本地设置,当前视觉效果仍会保留。</small></span>
133
+ <span><strong>设置未保存</strong><small>未能保存到 DSH,可能存在连接中断、配置冲突或当前访问不支持持久保存。重试将重新提交本次修改。</small></span>
128
134
  <button type="button" data-prts-retry-save>重试保存</button>
129
135
  </div>
130
136
  <div data-prts-reset-zone>
@@ -148,9 +154,14 @@ export function themeSettingsMarkup() {
148
154
  </div>
149
155
  <div data-prts-classic-options>${settingButtons('conversationEmblem', '正文徽记', [])}</div>
150
156
  ${avatarMarkup()}
151
- <div data-prts-baker-options>${settingButtons('bakerGroupAvatars', '合并过程回复头像', [])}</div>
157
+ <div data-prts-baker-options>${settingButtons('bakerGroupAvatars', '合并过程回复头像', [])}${settingButtons('bakerLoading', '回复完成前显示加载动画', [])}</div>
152
158
  </section>
153
159
  </section>
160
+ <section data-prts-settings-section="background" role="tabpanel" id="prts-page-background" aria-labelledby="prts-tab-background" tabindex="0" hidden>
161
+ ${backgroundMarkup()}
162
+ <div data-prts-composer-glass-setting>${settingButtons('composerGlass', '输入框增强毛玻璃', [])}</div>
163
+ <small data-prts-composer-glass-required hidden>自定义背景下,输入框增强毛玻璃始终开启。</small>
164
+ </section>
154
165
  <section data-prts-settings-common data-prts-settings-section="interaction" role="tabpanel" id="prts-page-interaction" aria-labelledby="prts-tab-interaction" tabindex="0" hidden>
155
166
  <div data-prts-settings-common-grid>
156
167
  ${COMMON_CONTROLS.filter(([key]) => ['cursor', 'railDefaultHidden', 'motion'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
@@ -222,7 +233,7 @@ export function createThemeSettingsOverlay({
222
233
  trigger,
223
234
  panel,
224
235
  backdrop,
225
- customEmblemStore, avatarStore,
236
+ customEmblemStore, avatarStore, backgroundStore,
226
237
  onOpen = () => {},
227
238
  onPreferenceChange = () => {},
228
239
  onPreferencePreview = () => {},
@@ -230,6 +241,7 @@ export function createThemeSettingsOverlay({
230
241
  onRetrySave = () => {},
231
242
  }) {
232
243
  const window = document?.defaultView
244
+ const backgroundEditor = mountBackgroundSettings({panel,window,store:backgroundStore,onChange:onPreferenceChange})
233
245
  const avatarEditor = mountAvatarSettings({panel,window,store:avatarStore})
234
246
  const customEditor = mountCustomEmblemEditor({ panel, window, store: customEmblemStore })
235
247
  const transparencyMedia = window?.matchMedia?.('(prefers-reduced-transparency: reduce)')
@@ -474,8 +486,8 @@ export function createThemeSettingsOverlay({
474
486
  if (phase === 'saved' && revision === renderedPersistenceRevision) return
475
487
  if (savedTimer !== undefined) window?.clearTimeout?.(savedTimer)
476
488
  savedTimer = undefined
477
- status.hidden = phase !== 'saved'
478
- status.textContent = phase === 'saved' ? '已保存' : ''
489
+ status.hidden = phase !== 'saved' && phase !== 'saving'
490
+ status.textContent = phase === 'saved' ? '已保存' : phase === 'saving' ? '正在保存…' : ''
479
491
  if (phase === 'saved') {
480
492
  renderedPersistenceRevision = revision
481
493
  savedTimer = window?.setTimeout?.(() => { status.hidden = true; savedTimer = undefined }, reducedMotion() ? 500 : 1200)
@@ -707,6 +719,10 @@ export function createThemeSettingsOverlay({
707
719
  panel.querySelector('[data-prts-classic-options]').hidden = preferences?.conversationStyle === 'deck-chat'
708
720
  panel.querySelector('[data-prts-baker-options]').hidden = preferences?.conversationStyle !== 'deck-chat'
709
721
  panel.querySelector('[data-prts-avatars]').hidden = preferences?.conversationStyle !== 'deck-chat'
722
+ const customBackground = preferences.backgroundMode !== 'off'
723
+ panel.querySelector('[data-prts-composer-glass-setting]').hidden = customBackground
724
+ panel.querySelector('[data-prts-composer-glass-required]').hidden = !customBackground
725
+ backgroundEditor.update(preferences)
710
726
  currentPreferences = preferences
711
727
  currentStatus = status
712
728
  currentPersistence = persistenceState ?? { phase: 'idle', revision: 0 }
@@ -723,6 +739,7 @@ export function createThemeSettingsOverlay({
723
739
  dispose() {
724
740
  customEditor.dispose()
725
741
  avatarEditor.dispose()
742
+ backgroundEditor.dispose()
726
743
  isOpen = false
727
744
  hidePanel()
728
745
  if (savedTimer !== undefined) window?.clearTimeout?.(savedTimer)
package/src/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { createBackgroundService, registerBackgroundApi } from './server/backgrounds.js'
2
+ import Schema from '@deepseek-ai/schemastery'
3
+ import { DEFAULT_PREFERENCES } from './client/preferences.js'
1
4
  import { createAvatarService, registerAvatarApi } from './server/avatars.js'
2
5
  import { join } from 'node:path'
3
6
  import { homedir } from 'node:os'
@@ -9,11 +12,22 @@ export const PACKAGE_ID = '@ash-qw/dsh-theme-prts'
9
12
  export const THEME_PLUGIN_ID = 'dsh-theme-prts'
10
13
  export const inject = ['tools', 'attachments', 'connection', 'webServer']
11
14
 
15
+ export const Config = Schema.object(Object.fromEntries([
16
+ ...Object.entries(DEFAULT_PREFERENCES).map(([key, value]) => [key,
17
+ (typeof value === 'boolean' ? Schema.boolean() : typeof value === 'number' ? Schema.number() : Schema.string()).default(value).volatile(),
18
+ ]),
19
+ ['preferencesInitialized', Schema.boolean().default(false).volatile()],
20
+ ]))
21
+
12
22
  export async function apply(ctx) {
23
+ ctx.inject(['settings'], settingsCtx => {
24
+ settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber))
25
+ })
13
26
  const directory=join(process.env.DSH_HOME?.trim() || join(homedir(),'.dsh'),'plugins','dsh-theme-prts','emblems')
14
27
  const service=await createEmblemService(directory)
28
+ const backgrounds=await createBackgroundService(join(directory,'..','backgrounds'))
15
29
  const avatars=await createAvatarService(join(directory,'..','avatars'))
16
30
  const disposers=[...emblemTools(service,ctx.attachments),...avatarTools(avatars,ctx.attachments)].map(tool=>ctx.tools.register(tool))
17
- disposers.push(registerEmblemApi(ctx,service),registerAvatarApi(ctx,avatars))
18
- ctx.effect(()=>async()=>{for(const dispose of disposers.reverse())dispose();await service.dispose()})
31
+ disposers.push(registerEmblemApi(ctx,service),registerAvatarApi(ctx,avatars),registerBackgroundApi(ctx,backgrounds))
32
+ ctx.effect(()=>async()=>{for(const dispose of disposers.reverse())dispose();await service.dispose();await backgrounds.dispose()})
19
33
  }
@@ -0,0 +1,97 @@
1
+ import { mkdir, readFile, writeFile, rename, rm } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { randomUUID } from 'node:crypto'
4
+ import sharp from 'sharp'
5
+ import { body, json, isBase64 } from './api.js'
6
+ import { fault } from './emblems.js'
7
+
8
+ export const BACKGROUND_API = '/prts-theme-api/backgrounds'
9
+ const BACKGROUND_SLOTS = ['full', 'sidebar', 'conversation']
10
+ export async function createBackgroundService(directory) {
11
+ await mkdir(directory, { recursive: true, mode: 0o700 })
12
+ const configurationPath = join(directory, 'configuration.json')
13
+ let state = { revision: 0, full: null, sidebar: null, conversation: null }
14
+ try { state = JSON.parse(await readFile(configurationPath, 'utf8')) } catch (error) { if (error.code !== 'ENOENT') throw error }
15
+ let queue = Promise.resolve()
16
+ const listeners = new Set()
17
+ const snapshot = () => structuredClone(state)
18
+ return {
19
+ snapshot,
20
+ subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn) },
21
+ async image(id) {
22
+ if (!BACKGROUND_SLOTS.some(slot => state[slot]?.id === id)) throw fault(404, '背景不存在')
23
+ return readFile(join(directory, id + '.webp'))
24
+ },
25
+ save(data) {
26
+ const task = queue.then(async () => {
27
+ if (!BACKGROUND_SLOTS.includes(data?.slot)) throw fault(400, '背景区域无效')
28
+ if (data.revision !== state.revision) throw fault(409, '背景已在其他页面更新,请重新操作')
29
+ let entry = null, output
30
+ if (data.image !== null) {
31
+ if (!isBase64(data.image) || data.image.length > 14 * 1024 * 1024) throw fault(400, '请上传不超过 10 MB 的图片')
32
+ const bytes = Buffer.from(data.image, 'base64')
33
+ if (!bytes.length || bytes.length > 10 * 1024 * 1024) throw fault(400, '请上传不超过 10 MB 的图片')
34
+ try {
35
+ const input = sharp(bytes, { limitInputPixels: 40000000, animated: false })
36
+ const metadata = await input.metadata()
37
+ if (!['png', 'jpeg', 'webp', 'avif'].includes(metadata.format)) throw Error('format')
38
+ const result = await input.rotate().resize({ width: 3840, height: 2160, fit: 'inside', withoutEnlargement: true }).webp({ quality: 86 }).toBuffer({ resolveWithObject: true })
39
+ output = result.data
40
+ entry = { id: randomUUID(), width: result.info.width, height: result.info.height }
41
+ } catch { throw fault(400, '无法读取图片,请使用 PNG、JPEG、WebP 或 AVIF(最多 4000 万像素)') }
42
+ }
43
+ const next = { ...state, revision: state.revision + 1, [data.slot]: entry }
44
+ const temporary = configurationPath + '.' + randomUUID() + '.tmp'
45
+ try {
46
+ if (entry) await writeFile(join(directory, entry.id + '.webp'), output, { mode: 0o600 })
47
+ await writeFile(temporary, JSON.stringify(next), { mode: 0o600 })
48
+ await rename(temporary, configurationPath)
49
+ } catch (error) {
50
+ if (entry) await rm(join(directory, entry.id + '.webp'), { force: true }).catch(() => {})
51
+ throw error
52
+ } finally { await rm(temporary, { force: true }).catch(() => {}) }
53
+ const previous = state[data.slot]
54
+ state = next
55
+ if (previous) await rm(join(directory, previous.id + '.webp'), { force: true }).catch(() => {})
56
+ for (const listener of listeners) { try { listener(state.revision) } catch {} }
57
+ return snapshot()
58
+ })
59
+ queue = task.catch(() => {})
60
+ return task
61
+ },
62
+ async dispose() { listeners.clear(); await queue },
63
+ }
64
+ }
65
+
66
+ export function registerBackgroundApi(ctx, service) {
67
+ const connection = ctx.connection
68
+ let disposed = false
69
+ const streams = new Set()
70
+ const unregister = ctx.webServer.register({ kind: 'prefix', path: BACKGROUND_API, handler: async (req, res) => {
71
+ try {
72
+ if (disposed) throw fault(503, '主题服务正在重新加载')
73
+ if ('rejection' in connection.admit(req)) throw fault(401, '请先登录 DSH')
74
+ const path = new URL(req.url, 'http://localhost').pathname.slice(BACKGROUND_API.length)
75
+ if (req.method === 'GET' && path === '/events') {
76
+ res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', 'x-accel-buffering': 'no' })
77
+ streams.add(res)
78
+ const send = revision => res.write('data: ' + JSON.stringify({ revision }) + '\n\n')
79
+ const unsubscribe = service.subscribe(send)
80
+ send(service.snapshot().revision)
81
+ const timer = setInterval(() => res.write(': heartbeat\n\n'), 25000)
82
+ res.on('close', () => { clearInterval(timer); unsubscribe(); streams.delete(res) })
83
+ return
84
+ }
85
+ if (req.method === 'GET' && /^\/image\/[a-f0-9-]{36}$/.test(path)) {
86
+ const bytes = await service.image(path.slice(7))
87
+ res.writeHead(200, { 'content-type': 'image/webp', 'cache-control': 'private, max-age=31536000, immutable', 'x-content-type-options': 'nosniff' }); res.end(bytes); return
88
+ }
89
+ if (path !== '/configuration') throw fault(404, '接口不存在')
90
+ if (req.method === 'GET') { json(res, 200, service.snapshot()); return }
91
+ if (req.method !== 'POST') throw fault(405, '请求方法不支持')
92
+ if (req.headers['x-prts-request'] !== '1') throw fault(403, '缺少请求标识')
93
+ json(res, 200, await service.save(await body(req)))
94
+ } catch (error) { if (!res.headersSent) json(res, error.status || 500, { error: error.message }); else res.end() }
95
+ } })
96
+ return () => { disposed = true; unregister(); for (const stream of streams) stream.end(); streams.clear() }
97
+ }
@@ -16,10 +16,12 @@
16
16
  html[data-dsh-prts][data-prts-cursor="prts"] :is(textarea, input:not([type]), input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], input[type="number"], input[type="tel"], [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]) {
17
17
  cursor: var(--prts-text-cursor), text;
18
18
  }
19
+ html[data-dsh-prts][data-prts-cursor="prts"] [data-prts-opacity-dial] svg,
19
20
  html[data-dsh-prts][data-prts-cursor="prts"] [data-emblem-key],
20
21
  html[data-dsh-prts][data-prts-cursor="prts"] :is(.dsh-partner-pendant-hit, .dsh-partner-pendant-anchor, [data-prts-emblem-reorder]):enabled {
21
22
  cursor: url("__PRTS_GRAB__") 22 19, grab;
22
23
  }
24
+ html[data-dsh-prts][data-prts-cursor="prts"] [data-prts-opacity-control][data-dragging] svg,
23
25
  html[data-dsh-prts][data-prts-cursor="prts"] :is(.dsh-partner-pendant-hit, .dsh-partner-pendant-anchor, [data-prts-emblem-reorder]):enabled:active {
24
26
  cursor: var(--prts-sort-cursor);
25
27
  }
@@ -0,0 +1,331 @@
1
+ /* Viewport-aligned wallpaper. */
2
+ html[data-dsh-prts] { --prts-background-wash: color-mix(in srgb, var(--prts-canvas) var(--prts-background-shade, 45%), transparent); }
3
+ html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] :is([data-prts-region="frame"], [data-prts-nav-rail]) {
4
+ background: linear-gradient(var(--prts-background-wash), var(--prts-background-wash)), var(--prts-background-full) center / cover fixed;
5
+ }
6
+ html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] :is([data-prts-region="sessions"], [data-prts-region="operation"]) { background: transparent; }
7
+ html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-sidebar] {
8
+ --prts-wall-width:max(calc(var(--prts-sidebar-width,280px) + var(--prts-nav-width)),calc((100dvh - var(--prts-host-top-inset)) * var(--prts-sidebar-image-ratio,1)));
9
+ --prts-wall-x:calc((var(--prts-sidebar-width,280px) + var(--prts-nav-width) - var(--prts-wall-width)) / 2);
10
+ & :is([data-prts-region="sessions"],[data-prts-nav-rail]){background:linear-gradient(var(--prts-background-wash),var(--prts-background-wash)),var(--prts-background-sidebar) var(--prts-wall-x) center / var(--prts-wall-width) auto no-repeat}
11
+ & [data-prts-region="sessions"]{background-position:center,calc(var(--prts-wall-x) - var(--prts-nav-width)) center}
12
+ }
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) center / cover no-repeat;
16
+ }
17
+ html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] [data-prts-ambient-layer],
18
+ html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-conversation] [data-prts-ambient-layer] { display: none !important; }
19
+ [data-prts-background-settings] { display: grid; gap: 14px; margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--prts-line); }
20
+ [data-prts-background-settings] header { display: grid; gap: 6px; }
21
+ [data-prts-background-settings] small, [data-prts-background-status] { color: var(--prts-muted); font-size: 12px; line-height: 1.6; }
22
+ [data-prts-background-modes] { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
23
+ [data-prts-background-modes] button { display: grid; gap: 10px; padding: 12px; border: 1px solid var(--prts-line); border-radius: 8px; background: transparent; color: var(--prts-ink); text-align: left; cursor: pointer; }
24
+ [data-prts-background-modes] button strong { display: block; font-size: 13px; margin-bottom: 3px; }
25
+ [data-prts-background-modes] button small { font-size: 11px; }
26
+ [data-prts-background-modes] button[aria-pressed="true"] { border-color: var(--prts-cyan); background: color-mix(in srgb, var(--prts-cyan) 8%, transparent); box-shadow: inset 0 0 0 1px var(--prts-cyan); }
27
+ [data-prts-background-diagram] { display: flex; gap: 3px; width: 44px; height: 26px; padding: 3px; border: 1px solid var(--prts-muted); border-radius: 4px; }
28
+ [data-prts-background-diagram] i { background: var(--prts-muted); opacity: .35; border-radius: 1px; flex: 1; }
29
+ [data-prts-background-diagram] i:first-child { flex: 0 0 28%; }
30
+ [data-prts-background-diagram="split"] i { background: var(--prts-cyan); opacity: .6; }
31
+ [data-prts-background-diagram="split"] i:first-child { opacity: .25; }
32
+ [data-prts-background-diagram="full"] { background: color-mix(in srgb, var(--prts-cyan) 25%, transparent); }
33
+ [data-prts-background-diagram="full"] i { background: transparent; }
34
+ [data-prts-background-images] { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
35
+ [data-prts-background-card="full"] { grid-column: 1 / -1; }
36
+ [data-prts-background-card-heading] { display: flex; justify-content: space-between; align-items: center; min-height: 30px; margin-bottom: 6px; font-size: 13px; }
37
+ [data-prts-background-upload] { position: relative; display: grid; place-items: center; width: 100%; height: 120px; padding: 0; border: 1px dashed var(--prts-line); border-radius: 8px; color: var(--prts-muted); background: var(--prts-panel); overflow: hidden; cursor: pointer; }
38
+ [data-prts-background-card="full"] [data-prts-background-upload] { height: 155px; }
39
+ [data-prts-background-upload][data-has-image] { border-style: solid; }
40
+ [data-prts-background-upload]:hover { border-color: var(--prts-cyan); color: var(--prts-ink); }
41
+ [data-prts-background-thumbnail] { position: absolute; inset: 0; background: center / cover no-repeat; }
42
+ [data-prts-background-caption] { position: relative; padding: 6px 12px; border-radius: 5px; font-size: 12px; }
43
+ [data-has-image] [data-prts-background-caption] { align-self: end; justify-self: end; margin: 8px; background: var(--prts-panel); color: var(--prts-ink); box-shadow: 0 1px 5px #0003; }
44
+ [data-prts-background-remove] { padding: 4px 8px; background: transparent; color: var(--prts-muted); border: 0; border-radius: 4px; font-size: 12px; cursor: pointer; }
45
+ [data-prts-background-remove]:hover { color: var(--prts-red); background: color-mix(in srgb, var(--prts-red) 8%, transparent); }
46
+ [data-prts-background-shade] { display: grid; gap: 8px; font-size: 13px; padding-top: 14px; border-top: 1px solid var(--prts-line); }
47
+ [data-prts-background-shade] > span { display: flex; justify-content: space-between; }
48
+ [data-prts-background-shade] output { color: var(--prts-muted); font-variant-numeric: tabular-nums; }
49
+ [data-prts-background-shade] input { width: 100%; accent-color: var(--prts-cyan); }
50
+ [data-prts-background-status] { margin: 0; }
51
+ [data-prts-background-status]:empty { display: none; }
52
+ [data-prts-background-settings] button:focus-visible { outline: 2px solid var(--prts-cyan); outline-offset: 3px; }
53
+ @media (max-width: 600px) {
54
+ [data-prts-background-modes] button { padding: 8px; }
55
+ [data-prts-background-modes] button small { display: none; }
56
+ [data-prts-background-images] { grid-template-columns: 1fr; }
57
+ }
58
+ [data-prts-background-settings] [hidden] { display: none !important; }
59
+ /* Classic conversation roots keep their native surface unless an image is active. */
60
+ html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] :is([data-slot="main.conversation"], [data-slot="conversation"]) > [data-phase],
61
+ html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-conversation] :is([data-slot="main.conversation"], [data-slot="conversation"]) > [data-phase] { background: transparent !important; }
62
+ /* The native fade ends in a solid sidebar color and would cover a custom image. */
63
+ html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] [data-prts-region="sessions"] [data-slot="sidebar.workspaces"] span[class$="_fade"],
64
+ 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; }
65
+
66
+ /* Filter the plate, not the ancestor of fixed controls. */
67
+ html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] [data-prts-region="sessions"]::before,
68
+ html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-sidebar] [data-prts-region="sessions"]::before {
69
+ content: "";
70
+ position: absolute;
71
+ inset: 0;
72
+ z-index: 0;
73
+ pointer-events: none;
74
+ background: color-mix(in srgb, var(--prts-panel) var(--prts-sidebar-opacity, 6%), transparent);
75
+ -webkit-backdrop-filter: blur(var(--prts-sidebar-blur, 8px)) saturate(1.15);
76
+ backdrop-filter: blur(var(--prts-sidebar-blur, 8px)) saturate(1.15);
77
+ box-shadow: inset -1px 0 color-mix(in srgb, var(--prts-ink) 10%, transparent), inset 0 1px rgb(255 255 255 / 8%);
78
+ }
79
+ /* Raise sidebar controls above glass. */
80
+ html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] [data-prts-region="sessions"] [data-slot="sidebar"] > :first-child,
81
+ html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-sidebar] [data-prts-region="sessions"] [data-slot="sidebar"] > :first-child {
82
+ position: relative;
83
+ z-index: 1;
84
+ }
85
+
86
+ html[data-dsh-prts]:is([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"][data-prts-background-sidebar]) {
87
+ /* Opacity on the parent isolates backdrop sampling during slide-in. */
88
+ &[data-prts-rail-mode="overlay"] [data-prts-nav-rail]{opacity:1;transition:transform 180ms var(--prts-ease-snap),visibility 0s linear 180ms}
89
+ &[data-prts-rail-mode="overlay"][data-prts-rail-open] [data-prts-nav-rail]{transition-delay:0s}
90
+ /* Glass stays below rail controls. */
91
+ & [data-prts-nav-rail]::before {
92
+ content: "";
93
+ position: absolute;
94
+ inset: 0;
95
+ z-index: -1;
96
+ pointer-events: none;
97
+ background: color-mix(in srgb, var(--prts-panel) var(--prts-rail-opacity, 30%), transparent);
98
+ -webkit-backdrop-filter: blur(var(--prts-rail-blur, 12px)) saturate(1.15);
99
+ backdrop-filter: blur(var(--prts-rail-blur, 12px)) saturate(1.15);
100
+ box-shadow: inset -1px 0 color-mix(in srgb, var(--prts-ink) 10%, transparent), inset 0 1px rgb(255 255 255 / 8%);
101
+ }
102
+
103
+ /* Glass rail feedback retains the wallpaper instead of switching to solid black. */
104
+ & [data-prts-nav-rail] button {
105
+ color: var(--prts-muted) !important;
106
+ border-bottom-color: var(--prts-line);
107
+ transition: background-color 140ms ease, box-shadow 140ms ease, color 140ms ease;
108
+ }
109
+ & [data-prts-nav-rail] button:is(:hover, :focus-visible, [aria-expanded="true"]) {
110
+ background: color-mix(in srgb, var(--prts-panel-raised) 32%, transparent);
111
+ color: var(--prts-ink) !important;
112
+ box-shadow: inset 0 1px rgb(255 255 255 / 15%), inset 0 0 0 1px color-mix(in srgb, var(--prts-ink) 10%, transparent);
113
+ }
114
+ & [data-prts-nav-rail] button:active {
115
+ background: color-mix(in srgb, var(--prts-panel-raised) 48%, transparent);
116
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--prts-ink) 18%, transparent);
117
+ }
118
+
119
+ /* Daylight needs a little contrast, rather than another white highlight. */
120
+ &[data-prts-scheme="light"] [data-prts-nav-rail] button:is(:hover, :focus-visible, [aria-expanded="true"]) {
121
+ background: color-mix(in srgb, var(--prts-ink) 8%, transparent);
122
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--prts-ink) 16%, transparent);
123
+ }
124
+ &[data-prts-scheme="light"] [data-prts-nav-rail] button:active {
125
+ background: color-mix(in srgb, var(--prts-ink) 13%, transparent);
126
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--prts-ink) 22%, transparent);
127
+ }
128
+
129
+ /* A dark solid-rail divider is too strong against the daylight wallpaper. */
130
+ &[data-prts-scheme="light"] [data-prts-nav-rail] {
131
+ border-right-color: color-mix(in srgb, var(--prts-ink) 10%, transparent);
132
+ box-shadow: none;
133
+ }
134
+ &[data-prts-scheme="light"] [data-prts-nav-rail]::before {
135
+ box-shadow: inset 0 1px rgb(255 255 255 / 8%);
136
+ }
137
+
138
+ }
139
+ /* The solid rail stays dark in both schemes; use the same light feedback
140
+ * instead of the conversation palette's light-mode ink/surface colors. */
141
+ html[data-dsh-prts]:not([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"][data-prts-background-sidebar]) [data-prts-nav-rail] button:is(:hover, :focus-visible, [aria-expanded="true"]) {
142
+ background: rgb(255 255 255 / 8%);
143
+ color: #f3f6f7 !important;
144
+ box-shadow: inset 0 0 0 1px rgb(255 255 255 / 12%);
145
+ }
146
+ html[data-dsh-prts]:not([data-prts-background-mode="full"][data-prts-background-full],[data-prts-background-mode="split"][data-prts-background-sidebar]) [data-prts-nav-rail] button:active {
147
+ background: rgb(255 255 255 / 14%);
148
+ color: #f3f6f7 !important;
149
+ box-shadow: inset 0 0 0 1px rgb(255 255 255 / 20%);
150
+ }
151
+
152
+ /* Baker keeps its game palette; only a real conversation wallpaper enables glass. */
153
+ html[data-dsh-prts][data-prts-conversation-style="deck-chat"]:is(
154
+ [data-prts-background-mode="full"][data-prts-background-full],
155
+ [data-prts-background-mode="split"][data-prts-background-conversation]
156
+ ) {
157
+ --prts-chat-user-surface: rgb(239 239 237 / var(--prts-baker-opacity, 50%));
158
+ --prts-chat-assistant-surface: rgb(69 70 68 / var(--prts-baker-opacity, 50%));
159
+ &[data-prts-scheme="light"] {
160
+ --prts-chat-user-surface: rgb(255 255 255 / var(--prts-baker-opacity, 50%));
161
+ --prts-chat-assistant-surface: rgb(223 227 223 / var(--prts-baker-opacity, 50%));
162
+ }
163
+ & :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"],
164
+ & :is([data-prts-ai-surface], [data-message-role="user"], [data-message-role="assistant"]),
165
+ & [data-chat-flow-kind="assistant-step"]:not([data-chat-group-part="reasoning"]) [class$="_body"]:has(:is([data-markdown], [data-slot*="markdown" i], .markdown-body)) {
166
+ -webkit-backdrop-filter: blur(var(--prts-baker-blur, 8px)) saturate(1.08);
167
+ backdrop-filter: blur(var(--prts-baker-blur, 8px)) saturate(1.08);
168
+ }
169
+ & [data-prts-ai-surface]::before,
170
+ & [data-message-role="assistant"]::after,
171
+ & :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"]::before,
172
+ & [data-message-role="user"]::before {
173
+ -webkit-backdrop-filter: blur(var(--prts-baker-blur, 8px)) saturate(1.08);
174
+ backdrop-filter: blur(var(--prts-baker-blur, 8px)) saturate(1.08);
175
+ }
176
+ }
177
+
178
+ /* Classic user messages share the wallpaper translucency controls with Baker bubbles. */
179
+ html[data-dsh-prts][data-prts-conversation-style="native"]:is(
180
+ [data-prts-background-mode="full"][data-prts-background-full],
181
+ [data-prts-background-mode="split"][data-prts-background-conversation]
182
+ ) {
183
+ & [data-message-role="user"],
184
+ & :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"] {
185
+ background: rgb(23 29 33 / var(--prts-classic-user-opacity, 50%)) !important;
186
+ -webkit-backdrop-filter: blur(var(--prts-classic-user-blur, 8px)) saturate(1.08);
187
+ backdrop-filter: blur(var(--prts-classic-user-blur, 8px)) saturate(1.08);
188
+ }
189
+ &[data-prts-scheme="light"] {
190
+ & [data-message-role="user"],
191
+ & :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"] {
192
+ background: rgb(229 234 237 / var(--prts-classic-user-opacity, 50%)) !important;
193
+ }
194
+ }
195
+ }
196
+
197
+ [data-prts-opacity-settings] { min-width: 0; max-width: 420px; margin: 0; padding: 14px 0 0; border: 0; border-top: 1px solid var(--prts-line); }
198
+ [data-prts-opacity-settings] legend { padding: 0 8px 0 0; font-size: 13px; font-weight: 600; }
199
+ [data-prts-opacity-grid] { display: grid; grid-template-columns: minmax(0,1fr); max-width: 420px; margin-top: 12px; }
200
+ [data-prts-opacity-control] { display: grid; grid-template-columns: minmax(116px,38%) minmax(0,1fr); grid-template-rows: 26px 96px auto auto; align-items: center; column-gap: 16px; min-width: 0; padding: 10px 0 12px; }
201
+ [data-prts-opacity-control]:not(:first-child):not([hidden]) { border-top: 1px solid var(--prts-line); }
202
+ [data-prts-opacity-control][hidden] { display: none; }
203
+ [data-prts-opacity-heading] { grid-column: 1; grid-row: 1; display: flex; align-self: stretch; align-items: center; justify-content: center; gap: 4px; min-width: 0; }
204
+ [data-prts-opacity-heading] > strong { font-size: 12px; line-height: 1.4; white-space: nowrap; }
205
+ [data-prts-opacity-control] > [data-prts-material-preview] { grid-column: 2; grid-row: 1 / 3; align-self: center; }
206
+ [data-prts-opacity-control] > [data-prts-opacity-note] { grid-column: 1 / -1; grid-row: 4; }
207
+ [data-prts-blur-control] { grid-column: 1 / -1; grid-row: 3; display: grid; grid-template-columns: minmax(0, 1fr) 36px 22px; align-items: center; gap: 8px; min-width: 0; margin-top: 6px; }
208
+ [data-prts-blur-control][hidden] { display: none; }
209
+ [data-prts-blur-control] label { display: grid; grid-template-columns: 34px minmax(0, 1fr); align-items: center; gap: 8px; min-width: 0; font-size: 11px; color: var(--prts-muted); }
210
+ [data-prts-blur-control] input { width: 100%; min-width: 0; margin: 0; accent-color: var(--prts-cyan); cursor: pointer; }
211
+ [data-prts-blur-control] output { color: var(--prts-ink); font: 11px/1 var(--prts-mono); text-align: right; font-variant-numeric: tabular-nums; }
212
+ [data-prts-blur-reset] { display: grid; place-items: center; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--prts-muted); cursor: pointer; }
213
+ [data-prts-blur-reset]:is(:hover, :focus-visible) { background: color-mix(in srgb, var(--prts-cyan) 12%, transparent); color: var(--prts-cyan); }
214
+ [data-prts-blur-reset] svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
215
+ [data-prts-opacity-dial] { position: relative; grid-column: 1; grid-row: 2; justify-self: center; width: 96px; aspect-ratio: 1; user-select: none; }
216
+ [data-prts-opacity-dial] svg { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; border-radius: 50%; }
217
+ [data-dragging] [data-prts-opacity-dial] svg { cursor: grabbing; }
218
+ [data-prts-opacity-dial] svg:focus-visible { outline: 2px solid var(--prts-cyan); outline-offset: 2px; }
219
+ [data-prts-opacity-dial] path { fill: none; stroke-width: 5; stroke-linecap: round; }
220
+ [data-dial-track] { stroke: var(--prts-muted); opacity: .25; }
221
+ [data-dial-lit], [data-dial-tail] { stroke: var(--prts-cyan); }
222
+ [data-prts-opacity-dial] [data-dial-tail] { stroke-width: 12; transition: opacity 160ms ease; }
223
+ [data-dial-head] { fill: var(--prts-cyan); }
224
+ [data-prts-opacity-number] { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); display: flex; align-items: baseline; gap: 1px; }
225
+ [data-prts-opacity-number] input { appearance: textfield; width: 3ch; padding: 3px 0; border: 0; border-bottom: 1px solid transparent; border-radius: 0; outline-offset: 2px; background: transparent; color: var(--prts-ink); text-align: center; font: 500 22px/1.2 var(--prts-mono); font-variant-numeric: tabular-nums; }
226
+ [data-prts-opacity-number] input:focus { border-bottom-color: var(--prts-cyan); }
227
+ [data-prts-opacity-number] input::-webkit-inner-spin-button, [data-prts-opacity-number] input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
228
+ [data-prts-opacity-number] span { font-size: 11px; color: var(--prts-muted); }
229
+ [data-prts-opacity-reset] { position: relative; display: grid; place-items: center; flex: 0 0 24px; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--prts-muted); cursor: pointer; }
230
+ [data-prts-opacity-reset] svg { display: block; width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
231
+ [data-prts-opacity-reset]:hover, [data-prts-opacity-reset]:focus-visible { background: color-mix(in srgb, var(--prts-cyan) 12%, transparent); color: var(--prts-cyan); }
232
+ [data-prts-opacity-reset]::after { content: attr(data-prts-opacity-reset-tooltip); position: absolute; z-index: 5; top: calc(100% + 6px); left: 0; padding: 5px 8px; border: 1px solid var(--prts-line); border-radius: 4px; background: var(--prts-panel-raised); color: var(--prts-ink); box-shadow: var(--prts-shadow-glass-overlay); white-space: nowrap; opacity: 0; transform: translateY(-3px); pointer-events: none; transition: opacity 120ms ease, transform 120ms ease; }
233
+ [data-prts-opacity-reset]:hover::after, [data-prts-opacity-reset]:focus-visible::after { opacity: 1; transform: translateY(0); }
234
+ [data-prts-opacity-control] [data-prts-opacity-note] { font-size: 10px; }
235
+ [data-prts-opacity-note]:empty { display: none; }
236
+ html[data-prts-motion="reduced"] [data-prts-opacity-dial] [data-dial-tail] { display: none; }
237
+ @media (prefers-reduced-motion: reduce) { [data-prts-opacity-dial] [data-dial-tail] { display: none; } }
238
+
239
+ /* Match the two peer modules without boxing one of them in a second surface. */
240
+ html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-section="appearance"] > section {
241
+ padding: 0;
242
+ border: 0;
243
+ border-radius: 0;
244
+ background: transparent;
245
+ }
246
+ html[data-dsh-prts] [data-prts-theme-settings] [data-prts-background-settings] {
247
+ margin-top: 24px;
248
+ padding-top: 12px;
249
+ border-top: 1px solid var(--prts-line);
250
+ }
251
+ /* SVG outlines are rectangular in Chromium. Draw keyboard focus inside the ring. */
252
+ html[data-dsh-prts] [data-prts-theme-settings] [data-prts-opacity-dial] svg:focus { outline: none !important; box-shadow: none; }
253
+ [data-dial-focus] { fill: none; stroke: var(--prts-cyan); stroke-width: 2; opacity: 0; }
254
+ [data-prts-opacity-dial] svg:focus-visible [data-dial-focus] { opacity: .8; }
255
+ [data-prts-opacity-control][data-dragging] [data-dial-focus] { opacity: 0; }
256
+ [data-prts-material-preview] {
257
+ position: relative;
258
+ width: 100%;
259
+ height: 104px;
260
+ overflow: hidden;
261
+ border: 1px solid var(--prts-line);
262
+ border-radius: 6px;
263
+ background: var(--prts-background-full, linear-gradient(135deg, #8bb6c2, #c7b69c)) center / cover;
264
+ --prts-preview-surface: var(--prts-panel);
265
+ }
266
+ 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)); }
267
+ html[data-prts-background-mode="split"] [data-prts-material-preview="bakerOpacity"] { background-image: var(--prts-background-conversation, linear-gradient(135deg, #8bb6c2, #c7b69c)); }
268
+ html[data-prts-background-mode="split"] [data-prts-material-preview="classicUserOpacity"] { background-image: var(--prts-background-conversation, linear-gradient(135deg, #8bb6c2, #c7b69c)); }
269
+ [data-prts-material-preview]::before { content: ""; position: absolute; inset: 0; background: var(--prts-background-wash); }
270
+ [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); }
271
+ [data-prts-material-preview-surface] i { display: block; height: 8px; border-radius: 2px; background: color-mix(in srgb, var(--prts-ink) 18%, transparent); }
272
+ [data-prts-material-preview-surface] i:last-child { width: 65%; }
273
+ [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); }
274
+ [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); }
275
+ 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%)); }
276
+ [data-prts-material-preview="classicUserOpacity"] [data-prts-material-preview-surface] { inset: 16px 10px; border-left: 3px solid var(--prts-yellow); border-radius: var(--prts-radius-card); background: rgb(23 29 33 / var(--prts-classic-user-opacity, 50%)); backdrop-filter: blur(var(--prts-classic-user-blur, 8px)) saturate(1.08); }
277
+ html[data-prts-scheme="light"] [data-prts-material-preview="classicUserOpacity"] [data-prts-material-preview-surface] { background: rgb(229 234 237 / var(--prts-classic-user-opacity, 50%)); }
278
+ @media (max-width: 560px) {
279
+ [data-prts-opacity-control] { column-gap: 10px; }
280
+ [data-prts-opacity-heading] { gap: 6px; }
281
+ [data-prts-opacity-dial] { width: 88px; }
282
+ }
283
+
284
+ /* Backgrounds now have their own settings page. */
285
+ html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-section="background"] > [data-prts-background-settings] { margin: 0; padding: 0; border: 0; }
286
+ html[data-dsh-prts] [data-prts-settings-section="background"] [data-prts-background-settings] > header { padding: 16px 0 10px; }
287
+ html[data-dsh-prts] [data-prts-settings-section="background"] [data-prts-background-settings] > header strong { font-size: 13px; color: var(--prts-muted); }
288
+ [data-prts-composer-glass-setting], [data-prts-composer-glass-required] { display: block; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--prts-line); }
289
+ [data-prts-composer-glass-required] { font-size: 12px; color: var(--prts-muted); }
290
+
291
+ /* The native active composer fades into DSH's base color. Baker paints a
292
+ * different solid backdrop, so match that backdrop while keeping the fade. */
293
+ html[data-dsh-prts][data-prts-conversation-style="deck-chat"][data-prts-background-mode="off"] [data-phase="active"] [data-composer-seat]:not(:has([data-prts-composer-glass-active])) {
294
+ background: linear-gradient(180deg, transparent 0, var(--prts-chat-dock-surface, var(--prts-chat-backdrop)) 36px) !important;
295
+ }
296
+ html[data-dsh-prts][data-prts-conversation-style="deck-chat"][data-prts-background-mode="off"][data-prts-texture="restrained"] {
297
+ --prts-chat-dock-surface: color-mix(in srgb, var(--prts-chat-backdrop) 82%, var(--prts-canvas));
298
+ }
299
+ html[data-dsh-prts][data-prts-conversation-style="deck-chat"][data-prts-background-mode="off"][data-prts-texture="off"] {
300
+ --prts-chat-dock-surface: color-mix(in srgb, var(--prts-chat-backdrop) 72%, var(--prts-canvas));
301
+ }
302
+
303
+ /* Native data-streaming disappears for settled and interrupted responses.
304
+ * Hide only the current response body, never reasoning, tools or earlier rounds. */
305
+ html[data-dsh-prts][data-prts-conversation-style="deck-chat"][data-prts-baker-loading]
306
+ [data-chat-flow-kind="assistant-step"]:not([data-chat-group-part="reasoning"])
307
+ [data-streaming] > :is([data-prts-ai-surface], [class$="_body"]) {
308
+ min-width: 78px;
309
+ width: 78px !important;
310
+ min-height: 42px;
311
+ box-sizing: border-box;
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: center;
315
+ padding: 12px 18px !important;
316
+ & > * { display: none !important; }
317
+ &::after {
318
+ content: "正在生成回复";
319
+ font-size: 0;
320
+ display: block;
321
+ width: 4px;
322
+ height: 4px;
323
+ border-radius: 1px;
324
+ animation: prts-baker-typing 1.2s linear infinite;
325
+ }
326
+ }
327
+ @keyframes prts-baker-typing {
328
+ 0%, 100% { background: color-mix(in srgb,currentColor 30%,transparent); box-shadow: -10px 0 0 currentColor, 10px 0 0 color-mix(in srgb,currentColor 30%,transparent); }
329
+ 33% { background: currentColor; box-shadow: -10px 0 0 color-mix(in srgb,currentColor 30%,transparent), 10px 0 0 color-mix(in srgb,currentColor 30%,transparent); }
330
+ 66% { background: color-mix(in srgb,currentColor 30%,transparent); box-shadow: -10px 0 0 color-mix(in srgb,currentColor 30%,transparent), 10px 0 0 currentColor; }
331
+ }