@ash-qw/dsh-theme-prts 2.4.0 → 2.5.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.
- package/CHANGELOG.md +23 -0
- package/README.en.md +7 -3
- package/README.md +7 -3
- package/lib/client.js +432 -74
- package/package.json +2 -2
- package/src/client/avatar-crop.js +7 -2
- package/src/client/composer-glass-surface.js +10 -1
- package/src/client/conversation-control-adapter.js +152 -3
- package/src/client/custom-background.js +53 -3
- package/src/client/custom-emblem.js +9 -3
- package/src/client/emblem-reorder.js +41 -10
- package/src/client/file-drop.js +48 -0
- package/src/client/theme-transfer-panel.js +58 -24
- package/src/client/theme-transfer.js +68 -26
- package/src/styles/custom-background.css +66 -6
- package/src/styles/prts.css +4 -2
- package/src/styles/refinement.css +41 -2
- package/src/styles/theme-settings-workbench.css +25 -8
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mountFileDrop } from './file-drop.js'
|
|
2
|
+
import { checkTransferCancelled, readTransferFile, THEME_PACKAGE_LIMIT, validateThemePackage, describeThemePackage, ignoredThemePackageFields } from './theme-transfer.js'
|
|
2
3
|
|
|
3
4
|
export function themeTransferMarkup() {
|
|
4
5
|
return `<dialog data-prts-transfer-dialog aria-labelledby="prts-transfer-title">
|
|
5
6
|
<section data-prts-transfer>
|
|
6
7
|
<header data-prts-transfer-heading><div><small>P.R.T.S. / 主题文件</small><h2 id="prts-transfer-title">导入与导出</h2></div><button type="button" data-prts-transfer-close aria-label="关闭导入与导出">×</button></header>
|
|
7
8
|
<div data-prts-transfer-columns>
|
|
8
|
-
<section data-prts-transfer-card>
|
|
9
|
+
<section data-prts-transfer-card="export">
|
|
9
10
|
<h3>导出主题</h3><p>保存一份主题,备份或带到另一台设备。</p>
|
|
10
11
|
<label data-prts-transfer-option><input type="checkbox" data-prts-transfer-assets checked><span>包含自定义素材<small>背景、头像和徽记,保留编辑参数与排列。</small></span></label>
|
|
11
12
|
<small>取消勾选,仅导出主题设置。</small>
|
|
12
13
|
<button type="button" data-prts-transfer-export>导出主题文件 <span aria-hidden="true">↓</span></button>
|
|
13
14
|
</section>
|
|
14
|
-
<section data-prts-transfer-card>
|
|
15
|
-
<h3>导入主题</h3><p
|
|
15
|
+
<section data-prts-transfer-card="import" data-prts-transfer-drop>
|
|
16
|
+
<h3>导入主题</h3><p>选择或拖入已导出的文件,确认内容后应用。</p>
|
|
16
17
|
<div data-prts-transfer-file-hint><span aria-hidden="true">↑</span><small>.prts-theme.json · 最大 160 MB</small></div>
|
|
17
|
-
<button type="button" data-prts-transfer-choose
|
|
18
|
+
<button type="button" data-prts-transfer-choose>选择主题文件 <span aria-hidden="true">↑</span></button>
|
|
18
19
|
<input type="file" accept=".prts-theme.json,application/json" aria-label="导入主题文件" data-prts-transfer-file hidden>
|
|
19
20
|
</section>
|
|
20
21
|
</div>
|
|
@@ -25,6 +26,8 @@ export function themeTransferMarkup() {
|
|
|
25
26
|
<div data-prts-transfer-buttons><button type="button" data-prts-transfer-apply>确认导入</button><button type="button" data-prts-transfer-cancel>取消</button></div>
|
|
26
27
|
</div>
|
|
27
28
|
<div data-prts-transfer-recovery hidden><small>恢复将替换最近一次导入后所做的修改。</small><button type="button" data-prts-transfer-restore>恢复导入前配置</button></div>
|
|
29
|
+
<div data-prts-transfer-progress hidden><div><span data-prts-transfer-phase></span><span data-prts-transfer-percent></span></div><progress data-prts-transfer-bar max="1" aria-label="主题文件处理进度"></progress></div>
|
|
30
|
+
<button type="button" data-prts-transfer-stop hidden>取消操作</button>
|
|
28
31
|
<p data-prts-transfer-status role="status" aria-live="polite"></p>
|
|
29
32
|
</section>
|
|
30
33
|
</dialog>`
|
|
@@ -40,34 +43,56 @@ export function mountThemeTransfer({ panel, window, transfer }) {
|
|
|
40
43
|
const keydown = event => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close() } }
|
|
41
44
|
const cancel = event => { if (event.target === dialog) { event.preventDefault(); event.stopPropagation(); close() } }
|
|
42
45
|
const get = name => section.querySelector(`[data-prts-transfer-${name}]`)
|
|
43
|
-
let pending, busy = false, disposed = false, generation = 0
|
|
46
|
+
let choosing = false, activeOperation, pending, busy = false, disposed = false, generation = 0, controller, cancellable = true
|
|
44
47
|
const render = () => {
|
|
45
48
|
if (disposed) return
|
|
49
|
+
for (const card of section.querySelectorAll('[data-prts-transfer-card]')) card.toggleAttribute('data-prts-transfer-active', card.dataset.prtsTransferCard === (busy ? activeOperation : pending || choosing ? 'import' : undefined))
|
|
46
50
|
get('preview').hidden = !pending
|
|
47
51
|
if (!pending) get('compatibility').hidden = true
|
|
48
52
|
get('recovery').hidden = !transfer.hasBackup
|
|
49
53
|
for (const control of section.querySelectorAll('button,input')) control.disabled = busy
|
|
54
|
+
get('stop').hidden = !busy
|
|
55
|
+
get('stop').disabled = !busy || !cancellable || controller?.signal.aborted
|
|
50
56
|
// Prevent local editors from racing a multi-service import; other clients use revision checks.
|
|
51
57
|
for (const page of panel.querySelectorAll('[data-prts-settings-section]')) if (!page.contains(section)) page.inert = busy
|
|
52
58
|
}
|
|
53
|
-
|
|
59
|
+
const progress = ({ value, label, cancellable: allowed }) => {
|
|
60
|
+
if (disposed) return
|
|
61
|
+
if (allowed === false) { cancellable = false; render() }
|
|
62
|
+
get('progress').hidden = false
|
|
63
|
+
get('phase').textContent = label
|
|
64
|
+
get('percent').textContent = value === null ? '' : `${Math.round(value * 100)}%`
|
|
65
|
+
if (value === null) get('bar').removeAttribute('value')
|
|
66
|
+
else get('bar').value = value
|
|
67
|
+
}
|
|
68
|
+
const paint = () => new Promise(resolve => window.setTimeout(resolve, 0))
|
|
69
|
+
async function run(kind, task) {
|
|
54
70
|
if (busy) return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
controller = new window.AbortController(); cancellable = true
|
|
72
|
+
activeOperation = kind
|
|
73
|
+
busy = true; section.setAttribute('aria-busy', 'true'); get('status').textContent = ''; get('progress').hidden = true; render()
|
|
74
|
+
progress({ value: null, label: '正在准备…' })
|
|
75
|
+
try {
|
|
76
|
+
await paint()
|
|
77
|
+
checkTransferCancelled(controller.signal)
|
|
78
|
+
const message = await task(controller.signal)
|
|
79
|
+
if (!disposed) { progress({ value: 1, label: '处理完成' }); get('status').textContent = message }
|
|
80
|
+
}
|
|
81
|
+
catch (error) { if (!disposed) { get('progress').hidden = true; get('status').textContent = error.name === 'AbortError' ? '操作已取消。' : error.message } }
|
|
82
|
+
finally { busy = false; if (!pending && !disposed) get('file').value = ''; if (!disposed) section.setAttribute('aria-busy', 'false'); render() }
|
|
59
83
|
}
|
|
60
84
|
const change = event => {
|
|
61
85
|
if (event.target !== get('file')) return
|
|
86
|
+
choosing = false
|
|
62
87
|
const file = event.target.files?.[0], selected = ++generation
|
|
63
88
|
pending = undefined; render()
|
|
64
89
|
if (!file) return
|
|
65
|
-
void run(async
|
|
90
|
+
void run('import', async signal => {
|
|
66
91
|
if (file.size > THEME_PACKAGE_LIMIT) throw Error('主题文件不能超过 160 MB')
|
|
67
|
-
const text = await
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
92
|
+
const text = await readTransferFile(window, file, signal, event => progress({ value: event.lengthComputable && event.total ? event.loaded / event.total * 0.8 : null, label: '正在读取主题文件…' }))
|
|
93
|
+
progress({ value: 0.8, label: '正在解析并检查主题文件…' })
|
|
94
|
+
await paint()
|
|
95
|
+
checkTransferCancelled(signal)
|
|
71
96
|
let value
|
|
72
97
|
try { value = JSON.parse(text) } catch { throw Error('主题文件不是有效的 JSON') }
|
|
73
98
|
validateThemePackage(value)
|
|
@@ -85,22 +110,31 @@ export function mountThemeTransfer({ panel, window, transfer }) {
|
|
|
85
110
|
}
|
|
86
111
|
const click = event => {
|
|
87
112
|
const button = event.target.closest?.('button')
|
|
113
|
+
if (button === get('stop') && busy && cancellable) {
|
|
114
|
+
controller.abort(); progress({ value: null, label: '正在取消,请稍候…', cancellable: false }); return
|
|
115
|
+
}
|
|
88
116
|
if (button === get('close')) close()
|
|
89
|
-
if (button === get('choose')) get('file').click()
|
|
90
|
-
if (button === get('export')) void run(async
|
|
91
|
-
const value = await transfer.export(get('assets').checked)
|
|
117
|
+
if (button === get('choose')) { choosing = true; render(); get('file').click() }
|
|
118
|
+
if (button === get('export')) void run('export', async signal => {
|
|
119
|
+
const value = await transfer.export(get('assets').checked, state => progress({ ...state, value: state.value * 0.9 }), signal)
|
|
120
|
+
progress({ value: 0.9, label: '正在生成下载文件…' })
|
|
121
|
+
await paint()
|
|
122
|
+
checkTransferCancelled(signal)
|
|
92
123
|
transfer.download(value)
|
|
93
124
|
return '已发起下载,请确认主题文件已保存。'
|
|
94
125
|
})
|
|
95
|
-
if (button === get('apply') && pending) void run(async
|
|
96
|
-
await transfer.import(pending); pending = undefined; get('file').value = ''
|
|
126
|
+
if (button === get('apply') && pending) void run('import', async signal => {
|
|
127
|
+
await transfer.import(pending, progress, signal); pending = undefined; get('file').value = ''
|
|
97
128
|
return '主题已导入并保存。'
|
|
98
129
|
})
|
|
99
|
-
if (button === get('cancel')) { pending = undefined; get('file').value = ''; get('status').textContent = ''; render() }
|
|
100
|
-
if (button === get('restore')) void run(async
|
|
130
|
+
if (button === get('cancel')) { pending = undefined; get('file').value = ''; get('status').textContent = ''; get('progress').hidden = true; render() }
|
|
131
|
+
if (button === get('restore')) void run('import', async signal => { await transfer.restore(progress, signal); return '已恢复导入前配置。' })
|
|
101
132
|
}
|
|
133
|
+
const drop = mountFileDrop({ area: get('drop'), input: get('file'), enabled: () => !busy && !disposed, onError: message => { get('status').textContent = message } })
|
|
134
|
+
const cancelChoice = () => { choosing = false; render() }
|
|
135
|
+
get('file').addEventListener('cancel', cancelChoice)
|
|
102
136
|
trigger?.addEventListener('click', open)
|
|
103
137
|
dialog.addEventListener('keydown', keydown); dialog.addEventListener('cancel', cancel)
|
|
104
138
|
section.addEventListener('change', change); section.addEventListener('click', click); render()
|
|
105
|
-
return { dispose() { disposed = true; generation++; if (dialog.open) dialog.close(); trigger?.removeEventListener('click', open); dialog.removeEventListener('keydown', keydown); dialog.removeEventListener('cancel', cancel); section.removeEventListener('change', change); section.removeEventListener('click', click) } }
|
|
139
|
+
return { dispose() { get('file').removeEventListener('cancel', cancelChoice); drop.dispose(); disposed = true; controller?.abort(); generation++; if (dialog.open) dialog.close(); trigger?.removeEventListener('click', open); dialog.removeEventListener('keydown', keydown); dialog.removeEventListener('cancel', cancel); section.removeEventListener('change', change); section.removeEventListener('click', click) } }
|
|
106
140
|
}
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { validBackgroundRegion } from '../shared/background-region.js'
|
|
2
2
|
import { DEFAULT_PREFERENCES, normalizePreferences } from './preferences.js'
|
|
3
3
|
|
|
4
|
+
export function checkTransferCancelled(signal) {
|
|
5
|
+
if (signal?.aborted) throw Object.assign(new Error('操作已取消'), { name: 'AbortError' })
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function readTransferFile(window, blob, signal, onProgress, asImage = false) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
checkTransferCancelled(signal)
|
|
11
|
+
const reader = new window.FileReader()
|
|
12
|
+
const cleanup = () => signal?.removeEventListener('abort', abort)
|
|
13
|
+
const abort = () => { reader.abort(); cleanup(); reject(Object.assign(new Error('操作已取消'), { name: 'AbortError' })) }
|
|
14
|
+
reader.onload = () => { cleanup(); resolve(asImage ? String(reader.result).split(',')[1] : reader.result) }
|
|
15
|
+
reader.onerror = () => { cleanup(); reject(Error('无法读取主题文件或图片')) }
|
|
16
|
+
reader.onprogress = onProgress
|
|
17
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
18
|
+
if (asImage) reader.readAsDataURL(blob)
|
|
19
|
+
else reader.readAsText(blob)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
export const THEME_PACKAGE_LIMIT = 160 * 1024 * 1024
|
|
5
24
|
const transferSlots = ['full', 'sidebar', 'conversation']
|
|
6
25
|
const transferRoles = ['user', 'assistant']
|
|
@@ -84,61 +103,64 @@ export function describeThemePackage(value) {
|
|
|
84
103
|
|
|
85
104
|
export function createThemeTransfer({ window, getPreferences, savePreferences, checkWritable, checkExport = checkWritable, themeVersion }) {
|
|
86
105
|
let busy = false, backup
|
|
87
|
-
const request = async (path, data) => {
|
|
88
|
-
|
|
106
|
+
const request = async (path, data, signal) => {
|
|
107
|
+
checkTransferCancelled(signal)
|
|
108
|
+
const response = await window.fetch('/prts-theme-api/' + path, { credentials: 'same-origin', signal,
|
|
89
109
|
...(data === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json', 'x-prts-request': '1' }, body: JSON.stringify(data) }) })
|
|
90
110
|
const result = await response.json()
|
|
91
111
|
if (!response.ok) throw Error(result.error || '无法连接主题素材服务')
|
|
92
112
|
return result
|
|
93
113
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
reader.onerror = () => reject(Error('无法读取背景图片'))
|
|
98
|
-
reader.readAsDataURL(blob)
|
|
99
|
-
})
|
|
100
|
-
async function capture(includeAssets) {
|
|
114
|
+
async function capture(includeAssets, progress = () => {}, signal) {
|
|
115
|
+
checkTransferCancelled(signal)
|
|
116
|
+
progress({ value: 0, label: '正在读取主题设置…' })
|
|
101
117
|
const value = { format: 'prts-theme', version: 1, themeVersion, exportedAt: new Date().toISOString(), preferences: { ...getPreferences() } }
|
|
102
118
|
const states = {}
|
|
103
119
|
if (includeAssets) {
|
|
104
120
|
const backgrounds = {}, backgroundRegions = {}, avatars = {}, items = [], ids = new Map()
|
|
105
|
-
|
|
121
|
+
progress({ value: 0.1, label: '正在读取背景…' })
|
|
122
|
+
states.backgrounds = await request('backgrounds/configuration', undefined, signal)
|
|
106
123
|
for (const slot of transferSlots) {
|
|
107
124
|
const entry = states.backgrounds[slot]
|
|
108
125
|
backgroundRegions[slot] = entry?.region ?? null
|
|
109
126
|
backgrounds[slot] = null
|
|
110
127
|
if (entry) {
|
|
111
128
|
if (!/^[a-f0-9-]{36}$/.test(entry.id)) throw Error('背景引用无效')
|
|
112
|
-
const response = await window.fetch('/prts-theme-api/backgrounds/image/' + entry.id, { credentials: 'same-origin' })
|
|
129
|
+
const response = await window.fetch('/prts-theme-api/backgrounds/image/' + entry.id, { credentials: 'same-origin', signal })
|
|
113
130
|
if (!response.ok) throw Error('无法导出背景图片')
|
|
114
|
-
backgrounds[slot] = await
|
|
131
|
+
backgrounds[slot] = await readTransferFile(window, await response.blob(), signal, undefined, true)
|
|
115
132
|
}
|
|
116
133
|
}
|
|
117
|
-
|
|
134
|
+
progress({ value: 0.35, label: '正在读取头像…' })
|
|
135
|
+
states.avatars = await request('avatars/configuration', undefined, signal)
|
|
118
136
|
for (const role of transferRoles) {
|
|
119
|
-
const source = await request('avatars/edit/' + role)
|
|
137
|
+
const source = await request('avatars/edit/' + role, undefined, signal)
|
|
120
138
|
if (source.revision !== states.avatars.revision) throw Error('头像已变化,请重新导出')
|
|
121
139
|
avatars[role] = source.image ? { image: source.image, crop: source.crop } : null
|
|
122
140
|
}
|
|
123
|
-
|
|
141
|
+
progress({ value: 0.5, label: '正在读取徽记…' })
|
|
142
|
+
states.emblems = await request('emblems/configuration', undefined, signal)
|
|
124
143
|
async function reference(entry) {
|
|
125
144
|
if (!entry) return null
|
|
126
145
|
if (entry.kind === 'builtin') return { kind: 'builtin', key: entry.key, label: entry.label }
|
|
127
146
|
if (!ids.has(entry.draftId)) {
|
|
128
147
|
ids.set(entry.draftId, items.length)
|
|
129
|
-
items.push(await request('emblems/draft/' + encodeURIComponent(entry.draftId) + '/export'))
|
|
148
|
+
items.push(await request('emblems/draft/' + encodeURIComponent(entry.draftId) + '/export', undefined, signal))
|
|
130
149
|
}
|
|
131
150
|
return { kind: 'custom', asset: ids.get(entry.draftId), label: entry.label }
|
|
132
151
|
}
|
|
133
152
|
const hero = await reference(states.emblems.configuration.hero), conversation = []
|
|
134
153
|
for (const entry of states.emblems.configuration.conversation) conversation.push(await reference(entry))
|
|
135
154
|
value.assets = { backgrounds, backgroundRegions, avatars, emblems: { items, hero, conversation } }
|
|
155
|
+
progress({ value: 0.85, label: '正在核对素材…' })
|
|
136
156
|
for (const kind of ['backgrounds', 'avatars', 'emblems']) {
|
|
137
|
-
if ((await request(kind + '/configuration')).revision !== states[kind].revision) throw Error('素材在导出期间发生变化,请重试')
|
|
157
|
+
if ((await request(kind + '/configuration', undefined, signal)).revision !== states[kind].revision) throw Error('素材在导出期间发生变化,请重试')
|
|
138
158
|
}
|
|
139
159
|
}
|
|
140
160
|
if (JSON.stringify(value.preferences) !== JSON.stringify(getPreferences())) throw Error('设置在导出期间发生变化,请重试')
|
|
161
|
+
checkTransferCancelled(signal)
|
|
141
162
|
validateThemePackage(value)
|
|
163
|
+
progress({ value: 1, label: '主题数据已就绪' })
|
|
142
164
|
return { value, states }
|
|
143
165
|
}
|
|
144
166
|
async function exclusive(fn) {
|
|
@@ -154,14 +176,23 @@ export function createThemeTransfer({ window, getPreferences, savePreferences, c
|
|
|
154
176
|
window.document.body.append(link); link.click(); link.remove()
|
|
155
177
|
window.setTimeout(() => window.URL.revokeObjectURL(url), 1000)
|
|
156
178
|
}
|
|
157
|
-
async function apply(value) {
|
|
179
|
+
async function apply(value, progress = () => {}, signal) {
|
|
180
|
+
checkTransferCancelled(signal)
|
|
158
181
|
validateThemePackage(value)
|
|
159
182
|
const preferenceRevision = checkWritable()
|
|
160
|
-
const before = await capture(Boolean(value.assets))
|
|
183
|
+
const before = await capture(Boolean(value.assets), state => progress({ value: state.value * 0.3, label: '备份:' + state.label }), signal)
|
|
161
184
|
backup = before.value
|
|
185
|
+
progress({ value: 0.3, label: '正在生成导入前备份…' })
|
|
186
|
+
await new Promise(resolve => window.setTimeout(resolve, 0))
|
|
187
|
+
checkTransferCancelled(signal)
|
|
162
188
|
download(backup, 'before-import')
|
|
189
|
+
let completed = 0
|
|
190
|
+
const total = value.assets ? value.assets.emblems.items.length + 7 : 1
|
|
191
|
+
const step = label => progress({ value: 0.35 + 0.65 * completed / total, label })
|
|
163
192
|
const undo = [], revisions = Object.fromEntries(Object.entries(before.states).map(([key, state]) => [key, state.revision]))
|
|
164
193
|
const write = async (kind, data, previous) => {
|
|
194
|
+
checkTransferCancelled(signal)
|
|
195
|
+
// Await writes without aborting: their confirmed revision is needed for rollback.
|
|
165
196
|
const result = await request(kind + '/configuration', { ...data, revision: revisions[kind] })
|
|
166
197
|
revisions[kind] = result.revision
|
|
167
198
|
undo.push(async () => {
|
|
@@ -173,24 +204,35 @@ export function createThemeTransfer({ window, getPreferences, savePreferences, c
|
|
|
173
204
|
if (value.assets) {
|
|
174
205
|
const a = value.assets, old = before.value.assets, drafts = []
|
|
175
206
|
// Generate every custom draft before changing the active configuration.
|
|
176
|
-
for (const item of a.emblems.items)
|
|
207
|
+
for (const item of a.emblems.items) {
|
|
208
|
+
step('正在导入徽记素材…')
|
|
209
|
+
checkTransferCancelled(signal)
|
|
210
|
+
drafts.push(await request('emblems/import', item)); completed++
|
|
211
|
+
}
|
|
177
212
|
const entry = (ref, index) => ref === null ? null : ref.kind === 'builtin' ? ref : { kind: 'custom', key: 'import-' + index + '-' + drafts[ref.asset].draftId, label: ref.label, draftId: drafts[ref.asset].draftId }
|
|
178
213
|
const configuration = { version: 2, hero: entry(a.emblems.hero, 'hero'), conversation: a.emblems.conversation.map(entry) }
|
|
179
|
-
for (const slot of transferSlots) await write('backgrounds', { slot, image: a.backgrounds[slot], region: a.backgroundRegions?.[slot] ?? null }, { slot, image: old.backgrounds[slot], region: old.backgroundRegions?.[slot] ?? null })
|
|
180
|
-
for (const role of transferRoles) await write('avatars', { role, image: a.avatars[role]?.image ?? null, crop: a.avatars[role]?.crop }, { role, image: old.avatars[role]?.image ?? null, crop: old.avatars[role]?.crop })
|
|
181
|
-
|
|
214
|
+
for (const slot of transferSlots) { step('正在应用背景…'); await write('backgrounds', { slot, image: a.backgrounds[slot], region: a.backgroundRegions?.[slot] ?? null }, { slot, image: old.backgrounds[slot], region: old.backgroundRegions?.[slot] ?? null }); completed++ }
|
|
215
|
+
for (const role of transferRoles) { step('正在应用头像…'); await write('avatars', { role, image: a.avatars[role]?.image ?? null, crop: a.avatars[role]?.crop }, { role, image: old.avatars[role]?.image ?? null, crop: old.avatars[role]?.crop }); completed++ }
|
|
216
|
+
step('正在应用徽记排列…')
|
|
217
|
+
await write('emblems', { configuration }, { configuration: before.states.emblems.configuration }); completed++
|
|
182
218
|
}
|
|
219
|
+
step('正在保存主题设置…')
|
|
220
|
+
checkTransferCancelled(signal)
|
|
221
|
+
progress({ value: 0.99, label: '正在保存主题设置…', cancellable: false })
|
|
183
222
|
await savePreferences(mergeThemePreferences(before.value.preferences, value.preferences), preferenceRevision)
|
|
223
|
+
progress({ value: 1, label: '主题已保存' })
|
|
184
224
|
} catch (error) {
|
|
225
|
+
progress({ value: null, label: signal?.aborted ? '正在取消,回退素材…' : '导入失败,正在回退素材…', cancellable: false })
|
|
185
226
|
let failures = 0
|
|
186
227
|
for (const restore of undo.reverse()) { try { await restore() } catch { failures++ } }
|
|
228
|
+
if (signal?.aborted && !failures && error.name === 'AbortError') throw Error('操作已取消,已回退已写入的素材。')
|
|
187
229
|
throw Error(`${error.message}。${failures ? '部分素材未能自动恢复。' : '已回退已确认写入的素材。'}若连接中断,请检查实际状态;导入前备份已发起下载,可重新导入恢复。`)
|
|
188
230
|
}
|
|
189
231
|
}
|
|
190
232
|
return {
|
|
191
|
-
export: includeAssets => exclusive(async () => { checkExport(); return (await capture(includeAssets)).value }),
|
|
192
|
-
import: value => exclusive(() => apply(value)),
|
|
193
|
-
restore: () => exclusive(() => { if (!backup) throw Error('没有可恢复的备份'); return apply(backup) }),
|
|
233
|
+
export: (includeAssets, progress, signal) => exclusive(async () => { checkExport(); return (await capture(includeAssets, progress, signal)).value }),
|
|
234
|
+
import: (value, progress, signal) => exclusive(() => apply(value, progress, signal)),
|
|
235
|
+
restore: (progress, signal) => exclusive(() => { if (!backup) throw Error('没有可恢复的备份'); return apply(backup, progress, signal) }),
|
|
194
236
|
get hasBackup() { return Boolean(backup) },
|
|
195
237
|
download,
|
|
196
238
|
}
|
|
@@ -187,18 +187,45 @@ html[data-dsh-prts][data-prts-conversation-style="deck-chat"]:is(
|
|
|
187
187
|
--prts-chat-user-surface: rgb(255 255 255 / var(--prts-baker-opacity, 50%));
|
|
188
188
|
--prts-chat-assistant-surface: rgb(223 227 223 / var(--prts-baker-opacity, 50%));
|
|
189
189
|
}
|
|
190
|
+
/* One backdrop plane covers the body and tail; the parent must not create
|
|
191
|
+
a nested backdrop root. The mask rounds the body without clipping content. */
|
|
190
192
|
& :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"],
|
|
191
|
-
& :is([data-prts-ai-surface], [data-message-role="user"], [data-message-role="assistant"])
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
backdrop-filter:
|
|
193
|
+
& :is([data-prts-ai-surface], [data-message-role="user"], [data-message-role="assistant"]) {
|
|
194
|
+
isolation: isolate;
|
|
195
|
+
background: transparent !important;
|
|
196
|
+
-webkit-backdrop-filter: none;
|
|
197
|
+
backdrop-filter: none;
|
|
195
198
|
}
|
|
196
199
|
& [data-prts-ai-surface]::before,
|
|
197
200
|
& [data-message-role="assistant"]::after,
|
|
198
201
|
& :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"]::before,
|
|
199
202
|
& [data-message-role="user"]::before {
|
|
200
|
-
-
|
|
201
|
-
|
|
203
|
+
inset: 0 0 0 -10px; width: auto; height: auto; z-index: -1;
|
|
204
|
+
transform: none;
|
|
205
|
+
-webkit-backdrop-filter: blur(var(--prts-baker-blur, 8px)) saturate(1.08);
|
|
206
|
+
backdrop-filter: blur(var(--prts-baker-blur, 8px)) saturate(1.08);
|
|
207
|
+
mask:
|
|
208
|
+
linear-gradient(#000 0 0) 10px 0 / calc(100% - 28px) calc(100% - 18px),
|
|
209
|
+
linear-gradient(#000 0 0) 10px 18px / calc(100% - 10px) calc(100% - 36px),
|
|
210
|
+
linear-gradient(#000 0 0) 28px bottom / calc(100% - 46px) 18px,
|
|
211
|
+
radial-gradient(circle at left bottom, #000 17.5px, transparent 18px) right top / 18px 18px,
|
|
212
|
+
radial-gradient(circle at right top, #000 17.5px, transparent 18px) 10px bottom / 18px 18px,
|
|
213
|
+
radial-gradient(circle at left top, #000 17.5px, transparent 18px) right bottom / 18px 18px,
|
|
214
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 18'%3E%3Cpath d='M0 0H10V18Q10 6 0 0Z'/%3E%3C/svg%3E") left top / 10px 18px;
|
|
215
|
+
mask-repeat: no-repeat;
|
|
216
|
+
}
|
|
217
|
+
& :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"]::before,
|
|
218
|
+
& [data-message-role="user"]::before {
|
|
219
|
+
inset: 0 -10px 0 0;
|
|
220
|
+
mask:
|
|
221
|
+
linear-gradient(#000 0 0) 18px 0 / calc(100% - 28px) calc(100% - 18px),
|
|
222
|
+
linear-gradient(#000 0 0) 0 18px / calc(100% - 10px) calc(100% - 36px),
|
|
223
|
+
linear-gradient(#000 0 0) 18px bottom / calc(100% - 46px) 18px,
|
|
224
|
+
radial-gradient(circle at right bottom, #000 17.5px, transparent 18px) left top / 18px 18px,
|
|
225
|
+
radial-gradient(circle at right top, #000 17.5px, transparent 18px) left bottom / 18px 18px,
|
|
226
|
+
radial-gradient(circle at left top, #000 17.5px, transparent 18px) right 10px bottom 0 / 18px 18px,
|
|
227
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 18'%3E%3Cpath d='M10 0H0V18Q0 6 10 0Z'/%3E%3C/svg%3E") right top / 10px 18px;
|
|
228
|
+
mask-repeat: no-repeat;
|
|
202
229
|
}
|
|
203
230
|
}
|
|
204
231
|
|
|
@@ -419,3 +446,36 @@ html[data-dsh-prts][data-windows-titlebar] [data-prts-region="operation"] { bord
|
|
|
419
446
|
[data-prts-region-handle="s"] {left:50%;bottom:-8px;transform:translateX(-50%);cursor:ns-resize}
|
|
420
447
|
[data-prts-region-handle="w"] {left:-8px;top:50%;transform:translateY(-50%);cursor:ew-resize}
|
|
421
448
|
[data-prts-region-handle="e"] {right:-8px;top:50%;transform:translateY(-50%);cursor:ew-resize}
|
|
449
|
+
|
|
450
|
+
html[data-dsh-prts] [data-prts-background-confirm] {
|
|
451
|
+
box-sizing: border-box; width: min(560px, calc(100vw - 32px)); max-height: calc(100dvh - 32px);
|
|
452
|
+
padding: 24px; overflow: auto; border: 1px solid var(--prts-line-strong); border-top: 2px solid var(--prts-yellow);
|
|
453
|
+
border-radius: 8px; background: var(--prts-canvas); color: var(--prts-ink); box-shadow: 0 24px 80px #0006;
|
|
454
|
+
font: 13px/1.6 var(--prts-body);
|
|
455
|
+
&::backdrop { background: #0008; backdrop-filter: blur(4px); }
|
|
456
|
+
& header { display: block; margin: 0 0 20px; padding: 0; border: 0; }
|
|
457
|
+
& header small { color: var(--prts-muted); font-size: 12px; }
|
|
458
|
+
& h2 { margin: 4px 0 0; color: var(--prts-ink); font-size: 21px; line-height: 1.4; }
|
|
459
|
+
& [data-prts-background-compare] { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
460
|
+
& figure { min-width: 0; margin: 0; }
|
|
461
|
+
& img { display: block; box-sizing: border-box; width: 100%; aspect-ratio: 4/3; object-fit: contain; border: 1px solid var(--prts-line); border-radius: 4px; background: var(--prts-panel-raised); }
|
|
462
|
+
& figure:last-child img { border-color: var(--prts-yellow); }
|
|
463
|
+
& figcaption { margin-top: 8px; font-size: 12px; }
|
|
464
|
+
& [data-prts-background-filename] { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--prts-muted); }
|
|
465
|
+
& p { margin: 20px 0; color: var(--prts-muted); font-size: 12px; }
|
|
466
|
+
& footer { display: flex; justify-content: flex-end; gap: 10px; padding-top: 16px; border-top: 1px solid var(--prts-line); }
|
|
467
|
+
& button { min-width: 88px; padding: 9px 16px; border: 1px solid var(--prts-line-strong); border-radius: 4px; background: var(--prts-panel-raised); color: var(--prts-ink); font: inherit; cursor: pointer; }
|
|
468
|
+
& [data-prts-background-confirm-apply] { border-color: var(--prts-yellow); background: var(--prts-yellow); color: #171b1d; font-weight: 600; }
|
|
469
|
+
& button:hover { filter: brightness(.94); }
|
|
470
|
+
& button:focus-visible { outline: 2px solid var(--prts-yellow); outline-offset: 3px; }
|
|
471
|
+
}
|
|
472
|
+
@media (max-width: 480px) {
|
|
473
|
+
html[data-dsh-prts] [data-prts-background-confirm] { padding: 18px; }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* Expanded process headers remain sticky without painting the native opaque base. */
|
|
477
|
+
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="operation"] :is([data-variant="think"][data-expanded] [data-open] > [data-disclosure-header],button[aria-expanded="true"]:has([data-compaction-disclosure])) {
|
|
478
|
+
background:color-mix(in srgb,var(--prts-panel) 28%,transparent);
|
|
479
|
+
-webkit-backdrop-filter:blur(12px);
|
|
480
|
+
backdrop-filter:blur(12px);
|
|
481
|
+
}
|
package/src/styles/prts.css
CHANGED
|
@@ -1542,7 +1542,7 @@ html[data-dsh-prts] [data-prts-conversation-control] {
|
|
|
1542
1542
|
|
|
1543
1543
|
html[data-dsh-prts] [data-composer-card] button:not([data-slot="conversation.input.attachments"] *):not([data-prts-glass-menu] *):not([data-prts-floating-glass] *):not([role="menu"] *):not([role="listbox"] *):is(:hover, :focus-visible),
|
|
1544
1544
|
html[data-dsh-prts] [data-prts-conversation-control]:is(:hover, :focus-visible) {
|
|
1545
|
-
border-color: var(--prts-
|
|
1545
|
+
border-color: var(--prts-line) !important;
|
|
1546
1546
|
}
|
|
1547
1547
|
|
|
1548
1548
|
html[data-dsh-prts] [data-composer-card] button[aria-label*="send" i],
|
|
@@ -1606,7 +1606,9 @@ html[data-dsh-prts] [data-prts-floating-item] {
|
|
|
1606
1606
|
color: var(--prts-ink) !important;
|
|
1607
1607
|
}
|
|
1608
1608
|
|
|
1609
|
-
|
|
1609
|
+
/* Native material menus own their item states, including body portals without
|
|
1610
|
+
aria-controls or composer markers. Keep generic fallback styling separate. */
|
|
1611
|
+
html[data-dsh-prts] [data-prts-floating-item]:not([data-menu-material] *, [data-prts-glass-menu] *, [data-slot="conversation.input.overlay"] *):is(:hover, [data-highlighted]) {
|
|
1610
1612
|
background: var(--prts-panel-muted) !important;
|
|
1611
1613
|
}
|
|
1612
1614
|
|
|
@@ -364,7 +364,7 @@ html[data-dsh-prts] :is(
|
|
|
364
364
|
[data-prts-glass-control],
|
|
365
365
|
[data-prts-conversation-control]:not([data-prts-conversation-control="micro"], [data-prts-conversation-control="file"])
|
|
366
366
|
):is(:hover, :focus-visible) {
|
|
367
|
-
--prts-control-outline: var(--prts-
|
|
367
|
+
--prts-control-outline: var(--prts-line);
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
html[data-dsh-prts] :is(
|
|
@@ -464,7 +464,7 @@ html[data-dsh-prts] :is([data-presented-file],[data-changed-files],[data-prts-co
|
|
|
464
464
|
background:var(--prts-active-glass)!important;color:var(--prts-ink);
|
|
465
465
|
backdrop-filter:var(--prts-active-glass-filter);
|
|
466
466
|
&::before{content:none}
|
|
467
|
-
&:is(:hover,:focus-within){border-color:var(--prts-
|
|
467
|
+
&:is(:hover,:focus-within){border-color:var(--prts-glass-border)!important}
|
|
468
468
|
}
|
|
469
469
|
/* One surface for both single-file summaries and grouped changes. */
|
|
470
470
|
html[data-dsh-prts] [data-changed-files]{
|
|
@@ -510,3 +510,42 @@ html[data-dsh-prts] [data-turn-trigger] {
|
|
|
510
510
|
& > div > p { color:var(--prts-muted); }
|
|
511
511
|
& [data-context-text] { color:var(--prts-ink); }
|
|
512
512
|
}
|
|
513
|
+
|
|
514
|
+
/* Body interactions lock onto four corners; shell controls keep their own treatment. */
|
|
515
|
+
html[data-dsh-prts] :is([data-chat-flow-kind],[data-message-role]) :is([data-prts-conversation-control],[data-presented-file],[data-changed-files],[data-produced-files-row] > button) {
|
|
516
|
+
position:relative;
|
|
517
|
+
--prts-control-outline:var(--prts-line);
|
|
518
|
+
&:is(:hover,:focus-visible,:focus-within) {
|
|
519
|
+
--prts-control-outline:var(--prts-line);
|
|
520
|
+
border-color:var(--prts-line)!important;
|
|
521
|
+
}
|
|
522
|
+
&:is([data-presented-file],[data-changed-files],[data-prts-conversation-control="file"]) {
|
|
523
|
+
&:is(:hover,:focus-visible,:focus-within){border-color:var(--prts-glass-border)!important}
|
|
524
|
+
}
|
|
525
|
+
&:active{transform:none}
|
|
526
|
+
}
|
|
527
|
+
html[data-dsh-prts] [data-prts-body-target-frame] {
|
|
528
|
+
position:fixed;inset:auto;left:0;top:0;width:0;height:0;margin:0;padding:0;border:0;background:transparent;overflow:visible;z-index:100;pointer-events:none;opacity:0;--prts-corner-width:1px;
|
|
529
|
+
& > i {
|
|
530
|
+
position:absolute;left:0;top:0;width:7px;height:7px;will-change:transform;
|
|
531
|
+
&::before,&::after{content:"";position:absolute;left:0;top:0;background:currentColor}
|
|
532
|
+
&::before{width:7px;height:var(--prts-corner-width);transform:scaleX(var(--prts-arm-scale,1));transform-origin:left center}
|
|
533
|
+
&::after{width:var(--prts-corner-width);height:7px;transform:scaleY(var(--prts-arm-scale,1));transform-origin:center top}
|
|
534
|
+
&:nth-child(2),&:nth-child(4){
|
|
535
|
+
&::before{transform-origin:right center}
|
|
536
|
+
&::after{left:auto;right:0}
|
|
537
|
+
}
|
|
538
|
+
&:nth-child(3),&:nth-child(4){
|
|
539
|
+
&::before{top:auto;bottom:0}
|
|
540
|
+
&::after{transform-origin:center bottom}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
&[data-pressed]{--prts-corner-width:2px;filter:brightness(1.3)}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
html[data-dsh-prts] :is([data-chat-flow-kind],[data-message-role]) [data-changed-files] button:is(:hover,:focus-visible) {
|
|
547
|
+
background:color-mix(in srgb,var(--prts-ink) 5%,transparent);box-shadow:none;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
html[data-dsh-prts] [data-prts-corner-target]:focus-visible{outline:none!important}
|
|
551
|
+
html[data-dsh-prts] [data-prts-corner-target]:active{transform:none!important}
|
|
@@ -940,10 +940,10 @@ html[data-dsh-prts] [data-prts-transfer] {
|
|
|
940
940
|
& input[type="file"] { max-width: 100%; min-width: 0; }
|
|
941
941
|
& input[type="checkbox"] { accent-color: var(--prts-yellow); }
|
|
942
942
|
& button { justify-self: start; padding: 8px 14px; border: 1px solid var(--prts-line-strong); border-radius: 0; background: var(--prts-panel-raised); color: var(--prts-ink); font: inherit; cursor: pointer; }
|
|
943
|
-
& button:hover:not(:disabled) { border-color: var(--prts-
|
|
943
|
+
& button:hover:not(:disabled) { border-color: var(--prts-line-strong); }
|
|
944
944
|
& button:focus-visible { outline: 2px solid var(--prts-yellow); outline-offset: 3px; }
|
|
945
945
|
& button:disabled { opacity: .55; cursor: wait; }
|
|
946
|
-
& [data-prts-transfer-
|
|
946
|
+
& [data-prts-transfer-apply] { border-color: var(--prts-yellow); }
|
|
947
947
|
& hr { width: 100%; border: 0; border-top: 1px solid var(--prts-line); }
|
|
948
948
|
& [data-prts-transfer-preview], & [data-prts-transfer-recovery] { display: grid; gap: 12px; padding: 16px; border: 1px solid var(--prts-line); }
|
|
949
949
|
& [hidden] { display: none !important; }
|
|
@@ -951,7 +951,7 @@ html[data-dsh-prts] [data-prts-transfer] {
|
|
|
951
951
|
|
|
952
952
|
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-actions] { display:flex; flex-direction:column; align-items:stretch; gap:8px; }
|
|
953
953
|
html[data-dsh-prts] [data-prts-transfer-open] { display:flex; align-items:center; gap:8px; width:100%; padding:10px 8px; background:none; border:1px solid var(--prts-line); border-radius:4px; color:var(--prts-ink); font:500 12px/1.4 var(--prts-body); cursor:pointer; }
|
|
954
|
-
html[data-dsh-prts] [data-prts-transfer-open]:hover { border-color:var(--prts-
|
|
954
|
+
html[data-dsh-prts] [data-prts-transfer-open]:hover { border-color:var(--prts-line); }
|
|
955
955
|
html[data-dsh-prts] [data-prts-transfer-dialog] { box-sizing:border-box; width:min(660px,calc(100vw - 32px)); max-height:calc(100dvh - 48px); padding:0; border:1px solid var(--prts-line-strong); border-radius:8px; background:var(--prts-canvas); color:var(--prts-ink); box-shadow:0 24px 80px #0006; font:13px/1.6 var(--prts-body); overflow:auto; }
|
|
956
956
|
html[data-dsh-prts] [data-prts-transfer-dialog]::backdrop { background:#0007; }
|
|
957
957
|
html[data-dsh-prts] [data-prts-transfer] {
|
|
@@ -960,10 +960,10 @@ html[data-dsh-prts] [data-prts-transfer] {
|
|
|
960
960
|
& h2 { margin:4px 0 0; font-size:20px; line-height:1.4; }
|
|
961
961
|
& h3 { margin:0; font-size:14px; }
|
|
962
962
|
& [data-prts-transfer-close] { padding:2px 10px; font-size:24px; border:0; background:none; }
|
|
963
|
-
& [data-prts-transfer-columns] { display:grid; grid-template-columns:1fr 1fr; gap:
|
|
964
|
-
& [data-prts-transfer-card] { display:flex; flex-direction:column; gap:12px; min-width:0; }
|
|
965
|
-
& [data-prts-transfer-card]
|
|
966
|
-
& [data-prts-transfer-card] > button {
|
|
963
|
+
& [data-prts-transfer-columns] { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
|
|
964
|
+
& [data-prts-transfer-card] { display:flex; flex-direction:column; gap:12px; min-width:0; padding:18px; border:1px solid var(--prts-line); border-radius:6px; background:var(--prts-panel-raised); transition:border-color 180ms ease,background-color 180ms ease,box-shadow 180ms ease; }
|
|
965
|
+
& [data-prts-transfer-card] > button { margin-top:auto; display:flex; align-items:center; justify-content:space-between; gap:12px; align-self:stretch; border-radius:4px; }
|
|
966
|
+
& [data-prts-transfer-card] > button:hover:not(:disabled) { border-color:var(--prts-line-strong); }
|
|
967
967
|
& [data-prts-transfer-option] { align-items:flex-start; flex-wrap:nowrap; padding:12px 0 0; }
|
|
968
968
|
& [data-prts-transfer-option] input { margin:5px 0 0; }
|
|
969
969
|
& [data-prts-transfer-option] small { display:block; margin-top:4px; }
|
|
@@ -986,7 +986,6 @@ html[data-dsh-prts] [data-prts-transfer] {
|
|
|
986
986
|
@media(max-width:540px) {
|
|
987
987
|
html[data-dsh-prts] [data-prts-transfer] { padding:20px; }
|
|
988
988
|
html[data-dsh-prts] [data-prts-transfer] [data-prts-transfer-columns] { grid-template-columns:1fr; gap:20px; }
|
|
989
|
-
html[data-dsh-prts] [data-prts-transfer] [data-prts-transfer-card] + section { padding:20px 0 0; border-left:0; border-top:1px solid var(--prts-line); }
|
|
990
989
|
}
|
|
991
990
|
|
|
992
991
|
/* Shared quiet actions below the settings categories. */
|
|
@@ -1015,3 +1014,21 @@ html[data-dsh-prts] [data-prts-theme-settings] [data-prts-footer-action][hidden]
|
|
|
1015
1014
|
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-actions] { display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:8px 12px; }
|
|
1016
1015
|
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-reset-zone] { width:100%; }
|
|
1017
1016
|
}
|
|
1017
|
+
|
|
1018
|
+
[data-prts-transfer-progress] { display: grid; gap: 8px; }
|
|
1019
|
+
[data-prts-transfer-progress] > div { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }
|
|
1020
|
+
[data-prts-transfer-bar] { width: 100%; height: 8px; accent-color: var(--prts-accent, #d8b85e); }
|
|
1021
|
+
|
|
1022
|
+
[data-prts-drop-active] { outline: 2px dashed var(--prts-accent, #d8b85e); outline-offset: -3px; }
|
|
1023
|
+
|
|
1024
|
+
html[data-dsh-prts] [data-prts-transfer] [data-prts-transfer-card][data-prts-transfer-active],
|
|
1025
|
+
html[data-dsh-prts] [data-prts-transfer]:not([aria-busy="true"]) [data-prts-transfer-card]:has(:focus-visible) {
|
|
1026
|
+
border-color:var(--prts-line-strong); background:color-mix(in srgb,var(--prts-ink) 3%,var(--prts-panel-raised)); box-shadow:inset 0 1px color-mix(in srgb,var(--prts-ink) 7%,transparent),0 8px 24px #0002;
|
|
1027
|
+
}
|
|
1028
|
+
@media (hover:hover) {
|
|
1029
|
+
html[data-dsh-prts] [data-prts-transfer]:not([aria-busy="true"]) [data-prts-transfer-card]:hover {
|
|
1030
|
+
border-color:var(--prts-line-strong); background:color-mix(in srgb,var(--prts-ink) 3%,var(--prts-panel-raised)); box-shadow:inset 0 1px color-mix(in srgb,var(--prts-ink) 7%,transparent),0 8px 24px #0002;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
html[data-prts-motion="reduced"] [data-prts-transfer-card] { transition:none; }
|
|
1034
|
+
@media (prefers-reduced-motion:reduce) { [data-prts-transfer-card] { transition:none; } }
|