@ash-qw/dsh-theme-prts 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.en.md +8 -0
- package/README.md +8 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/THIRD_PARTY_NOTICES.zh-CN.md +1 -1
- package/lib/client.js +378 -28
- package/package.json +2 -2
- package/src/client/conversation-control-adapter.js +2 -0
- package/src/client/custom-background.js +2 -1
- package/src/client/index.js +8 -0
- package/src/client/native-preferences.js +31 -0
- package/src/client/operations-shell-v2.js +2 -2
- package/src/client/particle-field-adapter.js +67 -14
- package/src/client/preferences.js +3 -1
- package/src/client/theme-controller-v2.js +2 -1
- package/src/client/theme-settings-workbench.js +13 -5
- package/src/client/theme-transfer-panel.js +98 -0
- package/src/client/theme-transfer.js +167 -0
- package/src/styles/custom-background.css +14 -0
- package/src/styles/refinement.css +22 -0
- package/src/styles/theme-settings-workbench.css +87 -2
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { DEFAULT_PREFERENCES, normalizePreferences } from './preferences.js'
|
|
2
|
+
|
|
3
|
+
export const THEME_PACKAGE_LIMIT = 160 * 1024 * 1024
|
|
4
|
+
const transferSlots = ['full', 'sidebar', 'conversation']
|
|
5
|
+
const transferRoles = ['user', 'assistant']
|
|
6
|
+
const transferBuiltins = ['rhodes-island', 'lungmen', 'penguin-logistics', 'rhine-lab', 'reunion']
|
|
7
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
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)
|
|
9
|
+
|
|
10
|
+
export function validateThemePackage(value) {
|
|
11
|
+
if (!record(value) || value.format !== 'prts-theme' || value.version !== 1) throw Error('不支持的主题文件格式或版本')
|
|
12
|
+
if (!record(value.preferences) || !Object.keys(value.preferences).some(key => key !== 'version' && Object.hasOwn(DEFAULT_PREFERENCES, key))) throw Error('主题文件缺少设置')
|
|
13
|
+
if (!Number.isInteger(value.preferences.version) || value.preferences.version < 1 || value.preferences.version > DEFAULT_PREFERENCES.version) throw Error('不支持此设置版本,请更新主题后重试')
|
|
14
|
+
const normalized = normalizePreferences(value.preferences)
|
|
15
|
+
for (const [key, input] of Object.entries(value.preferences)) {
|
|
16
|
+
if (!Object.hasOwn(DEFAULT_PREFERENCES, key) || key === 'version') continue
|
|
17
|
+
if (typeof input !== typeof DEFAULT_PREFERENCES[key] || (typeof input === 'number' && !Number.isFinite(input))) throw Error(`设置 ${key} 的类型无效`)
|
|
18
|
+
// Current-format files must not silently turn malformed settings into defaults.
|
|
19
|
+
if (value.preferences.version === DEFAULT_PREFERENCES.version && normalized[key] !== input) throw Error(`设置 ${key} 的值无效`)
|
|
20
|
+
}
|
|
21
|
+
if (value.assets === undefined) return value
|
|
22
|
+
const a = value.assets
|
|
23
|
+
if (!record(a) || !record(a.backgrounds) || !record(a.avatars) || !record(a.emblems)) throw Error('主题素材数据不完整')
|
|
24
|
+
for (const slot of transferSlots) if (a.backgrounds[slot] !== null && !encodedImage(a.backgrounds[slot], 10 * 1024 * 1024)) throw Error('背景图片数据无效')
|
|
25
|
+
for (const role of transferRoles) {
|
|
26
|
+
const avatar = a.avatars[role]
|
|
27
|
+
if (avatar === null) continue
|
|
28
|
+
const crop = avatar?.crop
|
|
29
|
+
if (!record(avatar) || !encodedImage(avatar.image, 5 * 1024 * 1024) || !record(crop)
|
|
30
|
+
|| !Number.isFinite(crop.zoom) || crop.zoom < 1 || crop.zoom > 4
|
|
31
|
+
|| !Number.isFinite(crop.x) || crop.x < 0 || crop.x > 1 || !Number.isFinite(crop.y) || crop.y < 0 || crop.y > 1) throw Error('头像数据或裁剪参数无效')
|
|
32
|
+
}
|
|
33
|
+
const { items, hero, conversation } = a.emblems
|
|
34
|
+
if (!Array.isArray(items) || items.length > 13 || !Array.isArray(conversation) || conversation.length < 1 || conversation.length > 12) throw Error('徽记数量无效')
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
if (!record(item) || item.format !== 'prts-emblem' || item.version !== 1 || typeof item.label !== 'string' || !item.label.trim() || item.label.length > 100 || !record(item.params) || !encodedImage(item.image, 8 * 1024 * 1024)) throw Error('徽记素材无效')
|
|
37
|
+
}
|
|
38
|
+
const validEntry = entry => record(entry) && (entry.kind === 'builtin' ? transferBuiltins.includes(entry.key) : entry.kind === 'custom' && Number.isInteger(entry.asset) && entry.asset >= 0 && entry.asset < items.length && typeof entry.label === 'string' && entry.label.length <= 100)
|
|
39
|
+
if ((hero !== null && (!validEntry(hero) || hero.kind !== 'custom')) || !conversation.every(validEntry)) throw Error('徽记引用无效')
|
|
40
|
+
return value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function mergeThemePreferences(current, imported) {
|
|
44
|
+
const normalized = normalizePreferences(imported)
|
|
45
|
+
const patch = Object.fromEntries(Object.keys(DEFAULT_PREFERENCES).filter(key => key !== 'version' && Object.hasOwn(imported, key)).map(key => [key, normalized[key]]))
|
|
46
|
+
return normalizePreferences({ ...current, ...patch, version: DEFAULT_PREFERENCES.version })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function describeThemePackage(value) {
|
|
50
|
+
const count = Object.keys(value.preferences).filter(key => key !== 'version' && Object.hasOwn(DEFAULT_PREFERENCES, key)).length
|
|
51
|
+
if (!value.assets) return `${count} 项主题设置;不包含素材,将保留当前背景、头像和徽记。`
|
|
52
|
+
const a = value.assets
|
|
53
|
+
return `${count} 项主题设置、${transferSlots.filter(k => a.backgrounds[k]).length} 张背景、${transferRoles.filter(k => a.avatars[k]).length} 个头像、${a.emblems.items.length} 个自定义徽记。将替换当前素材及徽记排列,空项恢复为默认。`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createThemeTransfer({ window, getPreferences, savePreferences, checkWritable, checkExport = checkWritable, themeVersion }) {
|
|
57
|
+
let busy = false, backup
|
|
58
|
+
const request = async (path, data) => {
|
|
59
|
+
const response = await window.fetch('/prts-theme-api/' + path, { credentials: 'same-origin',
|
|
60
|
+
...(data === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json', 'x-prts-request': '1' }, body: JSON.stringify(data) }) })
|
|
61
|
+
const result = await response.json()
|
|
62
|
+
if (!response.ok) throw Error(result.error || '无法连接主题素材服务')
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
const readImage = blob => new Promise((resolve, reject) => {
|
|
66
|
+
const reader = new window.FileReader()
|
|
67
|
+
reader.onload = () => resolve(String(reader.result).split(',')[1])
|
|
68
|
+
reader.onerror = () => reject(Error('无法读取背景图片'))
|
|
69
|
+
reader.readAsDataURL(blob)
|
|
70
|
+
})
|
|
71
|
+
async function capture(includeAssets) {
|
|
72
|
+
const value = { format: 'prts-theme', version: 1, themeVersion, exportedAt: new Date().toISOString(), preferences: { ...getPreferences() } }
|
|
73
|
+
const states = {}
|
|
74
|
+
if (includeAssets) {
|
|
75
|
+
const backgrounds = {}, avatars = {}, items = [], ids = new Map()
|
|
76
|
+
states.backgrounds = await request('backgrounds/configuration')
|
|
77
|
+
for (const slot of transferSlots) {
|
|
78
|
+
const entry = states.backgrounds[slot]
|
|
79
|
+
backgrounds[slot] = null
|
|
80
|
+
if (entry) {
|
|
81
|
+
if (!/^[a-f0-9-]{36}$/.test(entry.id)) throw Error('背景引用无效')
|
|
82
|
+
const response = await window.fetch('/prts-theme-api/backgrounds/image/' + entry.id, { credentials: 'same-origin' })
|
|
83
|
+
if (!response.ok) throw Error('无法导出背景图片')
|
|
84
|
+
backgrounds[slot] = await readImage(await response.blob())
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
states.avatars = await request('avatars/configuration')
|
|
88
|
+
for (const role of transferRoles) {
|
|
89
|
+
const source = await request('avatars/edit/' + role)
|
|
90
|
+
if (source.revision !== states.avatars.revision) throw Error('头像已变化,请重新导出')
|
|
91
|
+
avatars[role] = source.image ? { image: source.image, crop: source.crop } : null
|
|
92
|
+
}
|
|
93
|
+
states.emblems = await request('emblems/configuration')
|
|
94
|
+
async function reference(entry) {
|
|
95
|
+
if (!entry) return null
|
|
96
|
+
if (entry.kind === 'builtin') return { kind: 'builtin', key: entry.key, label: entry.label }
|
|
97
|
+
if (!ids.has(entry.draftId)) {
|
|
98
|
+
ids.set(entry.draftId, items.length)
|
|
99
|
+
items.push(await request('emblems/draft/' + encodeURIComponent(entry.draftId) + '/export'))
|
|
100
|
+
}
|
|
101
|
+
return { kind: 'custom', asset: ids.get(entry.draftId), label: entry.label }
|
|
102
|
+
}
|
|
103
|
+
const hero = await reference(states.emblems.configuration.hero), conversation = []
|
|
104
|
+
for (const entry of states.emblems.configuration.conversation) conversation.push(await reference(entry))
|
|
105
|
+
value.assets = { backgrounds, avatars, emblems: { items, hero, conversation } }
|
|
106
|
+
for (const kind of ['backgrounds', 'avatars', 'emblems']) {
|
|
107
|
+
if ((await request(kind + '/configuration')).revision !== states[kind].revision) throw Error('素材在导出期间发生变化,请重试')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (JSON.stringify(value.preferences) !== JSON.stringify(getPreferences())) throw Error('设置在导出期间发生变化,请重试')
|
|
111
|
+
validateThemePackage(value)
|
|
112
|
+
return { value, states }
|
|
113
|
+
}
|
|
114
|
+
async function exclusive(fn) {
|
|
115
|
+
if (busy) throw Error('正在处理主题文件,请稍候')
|
|
116
|
+
busy = true
|
|
117
|
+
try { return await fn() } finally { busy = false }
|
|
118
|
+
}
|
|
119
|
+
function download(value, name = 'theme') {
|
|
120
|
+
const blob = new window.Blob([JSON.stringify(value)], { type: 'application/json' })
|
|
121
|
+
if (blob.size > THEME_PACKAGE_LIMIT) throw Error('主题文件超过 160 MB,请减少素材后重试')
|
|
122
|
+
const url = window.URL.createObjectURL(blob), link = window.document.createElement('a')
|
|
123
|
+
link.href = url; link.download = `prts-${name}-${new Date().toISOString().replace(/[:.]/g, '-')}.prts-theme.json`
|
|
124
|
+
window.document.body.append(link); link.click(); link.remove()
|
|
125
|
+
window.setTimeout(() => window.URL.revokeObjectURL(url), 1000)
|
|
126
|
+
}
|
|
127
|
+
async function apply(value) {
|
|
128
|
+
validateThemePackage(value)
|
|
129
|
+
const preferenceRevision = checkWritable()
|
|
130
|
+
const before = await capture(Boolean(value.assets))
|
|
131
|
+
backup = before.value
|
|
132
|
+
download(backup, 'before-import')
|
|
133
|
+
const undo = [], revisions = Object.fromEntries(Object.entries(before.states).map(([key, state]) => [key, state.revision]))
|
|
134
|
+
const write = async (kind, data, previous) => {
|
|
135
|
+
const result = await request(kind + '/configuration', { ...data, revision: revisions[kind] })
|
|
136
|
+
revisions[kind] = result.revision
|
|
137
|
+
undo.push(async () => {
|
|
138
|
+
const restored = await request(kind + '/configuration', { ...previous, revision: revisions[kind] })
|
|
139
|
+
revisions[kind] = restored.revision
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
if (value.assets) {
|
|
144
|
+
const a = value.assets, old = before.value.assets, drafts = []
|
|
145
|
+
// Generate every custom draft before changing the active configuration.
|
|
146
|
+
for (const item of a.emblems.items) drafts.push(await request('emblems/import', item))
|
|
147
|
+
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 }
|
|
148
|
+
const configuration = { version: 2, hero: entry(a.emblems.hero, 'hero'), conversation: a.emblems.conversation.map(entry) }
|
|
149
|
+
for (const slot of transferSlots) await write('backgrounds', { slot, image: a.backgrounds[slot] }, { slot, image: old.backgrounds[slot] })
|
|
150
|
+
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 })
|
|
151
|
+
await write('emblems', { configuration }, { configuration: before.states.emblems.configuration })
|
|
152
|
+
}
|
|
153
|
+
await savePreferences(mergeThemePreferences(before.value.preferences, value.preferences), preferenceRevision)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
let failures = 0
|
|
156
|
+
for (const restore of undo.reverse()) { try { await restore() } catch { failures++ } }
|
|
157
|
+
throw Error(`${error.message}。${failures ? '部分素材未能自动恢复。' : '已回退已确认写入的素材。'}若连接中断,请检查实际状态;导入前备份已发起下载,可重新导入恢复。`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
export: includeAssets => exclusive(async () => { checkExport(); return (await capture(includeAssets)).value }),
|
|
162
|
+
import: value => exclusive(() => apply(value)),
|
|
163
|
+
restore: () => exclusive(() => { if (!backup) throw Error('没有可恢复的备份'); return apply(backup) }),
|
|
164
|
+
get hasBackup() { return Boolean(backup) },
|
|
165
|
+
download,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -329,3 +329,17 @@ animation: prts-baker-typing 1.2s linear infinite;
|
|
|
329
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
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
331
|
}
|
|
332
|
+
|
|
333
|
+
/* Keep the hero treatment beside the background readability controls. */
|
|
334
|
+
html[data-dsh-prts] [data-prts-background-hero-setting] {
|
|
335
|
+
padding:16px 0 2px; border-top:1px solid var(--prts-line);
|
|
336
|
+
& [data-prts-setting-row] { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:20px; margin:0; padding:0; border:0; background:none; }
|
|
337
|
+
& [data-prts-setting-row] > span { display:grid; gap:5px; min-width:0; }
|
|
338
|
+
& strong { font:500 13px/1.5 var(--prts-body); color:var(--prts-ink); }
|
|
339
|
+
& small { max-width:36em; font:400 12px/1.6 var(--prts-body); color:var(--prts-muted); }
|
|
340
|
+
& [data-prts-setting-switch] { justify-self:end; align-self:center; margin:0; }
|
|
341
|
+
}
|
|
342
|
+
html[data-dsh-prts] [data-prts-background-hero-setting][hidden] { display:none; }
|
|
343
|
+
|
|
344
|
+
/* Windows chrome must not clip a different canvas color into the conversation. */
|
|
345
|
+
html[data-dsh-prts][data-windows-titlebar] [data-prts-region="operation"] { border-radius: 0; }
|
|
@@ -482,3 +482,25 @@ html[data-dsh-prts][data-prts-conversation-style="deck-chat"] :is([class$="_bubb
|
|
|
482
482
|
padding:2px 7px!important;border-radius:6px!important;
|
|
483
483
|
&:is(:hover,:focus-visible){border-color:var(--prts-cyan)!important}
|
|
484
484
|
}
|
|
485
|
+
|
|
486
|
+
/* Notification cards own one surface across the disclosure and its content. */
|
|
487
|
+
html[data-dsh-prts] [data-turn-trigger] {
|
|
488
|
+
background:var(--prts-glass-soft-surface);
|
|
489
|
+
border:1px solid var(--prts-line);
|
|
490
|
+
border-radius:var(--dsw-radius-xl,16px);
|
|
491
|
+
-webkit-backdrop-filter:blur(12px);
|
|
492
|
+
backdrop-filter:blur(12px);
|
|
493
|
+
color:var(--prts-ink);
|
|
494
|
+
& > button {
|
|
495
|
+
background:transparent;
|
|
496
|
+
border:0;
|
|
497
|
+
border-radius:inherit;
|
|
498
|
+
color:inherit;
|
|
499
|
+
&:hover { background:color-mix(in srgb,var(--prts-ink) 5%,transparent); }
|
|
500
|
+
&:focus-visible { outline:2px solid var(--prts-cyan); outline-offset:-3px; }
|
|
501
|
+
& time { color:var(--prts-muted); }
|
|
502
|
+
}
|
|
503
|
+
& > div { border-top:1px solid var(--prts-line); }
|
|
504
|
+
& > div > p { color:var(--prts-muted); }
|
|
505
|
+
& [data-context-text] { color:var(--prts-ink); }
|
|
506
|
+
}
|
|
@@ -757,9 +757,9 @@ html[data-dsh-prts] [data-prts-theme-settings] {
|
|
|
757
757
|
& [data-prts-reset-confirm] > span { flex-basis:100%; }
|
|
758
758
|
@media(max-width:720px) {
|
|
759
759
|
& [data-prts-settings-sidebar] { flex-direction:row; border:0; border-bottom:1px solid var(--prts-line); }
|
|
760
|
-
& [data-prts-settings-nav] { flex:1; padding:8px 4px; border:0; }
|
|
760
|
+
& [data-prts-settings-nav] { flex:1; min-width:0; overflow-x:auto; padding:8px 4px; border:0; }
|
|
761
761
|
& [data-prts-settings-jump] { padding:10px 8px; }
|
|
762
|
-
& [data-prts-settings-actions] { margin:0; padding:8px 4px; align-self:center; }
|
|
762
|
+
& [data-prts-settings-actions] { flex:0 0 auto; white-space:nowrap; margin:0; padding:8px 4px; align-self:center; }
|
|
763
763
|
& [data-prts-reset-confirm], & [data-prts-persistence-error] { left:auto; right:8px; top:100%; bottom:auto; max-width:calc(100vw - 32px); }
|
|
764
764
|
}
|
|
765
765
|
& [data-prts-reset-zone] { padding: 0; }
|
|
@@ -929,3 +929,88 @@ html[data-dsh-prts] [data-prts-ai-tools] blockquote { margin:12px 0;padding:8px
|
|
|
929
929
|
html[data-dsh-prts] [data-prts-ai-tools] summary { cursor:pointer; }
|
|
930
930
|
html[data-dsh-prts] [data-prts-ai-tools] dt { margin-top:12px; }
|
|
931
931
|
html[data-dsh-prts] [data-prts-ai-tools] dd { margin:3px 0 0; }
|
|
932
|
+
|
|
933
|
+
html[data-dsh-prts] [data-prts-transfer] {
|
|
934
|
+
display: grid;
|
|
935
|
+
gap: 16px;
|
|
936
|
+
padding-block: 16px;
|
|
937
|
+
& header { display: grid; gap: 8px; }
|
|
938
|
+
& p, & small { margin: 0; color: var(--prts-muted); line-height: 1.7; overflow-wrap: anywhere; }
|
|
939
|
+
& label { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
|
|
940
|
+
& input[type="file"] { max-width: 100%; min-width: 0; }
|
|
941
|
+
& input[type="checkbox"] { accent-color: var(--prts-yellow); }
|
|
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-yellow); }
|
|
944
|
+
& button:focus-visible { outline: 2px solid var(--prts-yellow); outline-offset: 3px; }
|
|
945
|
+
& button:disabled { opacity: .55; cursor: wait; }
|
|
946
|
+
& [data-prts-transfer-export], & [data-prts-transfer-apply] { border-color: var(--prts-yellow); }
|
|
947
|
+
& hr { width: 100%; border: 0; border-top: 1px solid var(--prts-line); }
|
|
948
|
+
& [data-prts-transfer-preview], & [data-prts-transfer-recovery] { display: grid; gap: 12px; padding: 16px; border: 1px solid var(--prts-line); }
|
|
949
|
+
& [hidden] { display: none !important; }
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-actions] { display:flex; flex-direction:column; align-items:stretch; gap:8px; }
|
|
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-yellow); }
|
|
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
|
+
html[data-dsh-prts] [data-prts-transfer-dialog]::backdrop { background:#0007; }
|
|
957
|
+
html[data-dsh-prts] [data-prts-transfer] {
|
|
958
|
+
padding:24px; gap:20px;
|
|
959
|
+
& [data-prts-transfer-heading] { display:flex; align-items:center; justify-content:space-between; gap:16px; }
|
|
960
|
+
& h2 { margin:4px 0 0; font-size:20px; line-height:1.4; }
|
|
961
|
+
& h3 { margin:0; font-size:14px; }
|
|
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:24px; }
|
|
964
|
+
& [data-prts-transfer-card] { display:flex; flex-direction:column; gap:12px; min-width:0; }
|
|
965
|
+
& [data-prts-transfer-card] + section { padding-left:24px; border-left:1px solid var(--prts-line); }
|
|
966
|
+
& [data-prts-transfer-card] > button { margin-top:auto; display:flex; align-items:center; justify-content:space-between; }
|
|
967
|
+
& [data-prts-transfer-option] { align-items:flex-start; flex-wrap:nowrap; padding:12px 0 0; }
|
|
968
|
+
& [data-prts-transfer-option] input { margin:5px 0 0; }
|
|
969
|
+
& [data-prts-transfer-option] small { display:block; margin-top:4px; }
|
|
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
|
+
& [data-prts-transfer-file-hint] > span { font-size:24px; color:var(--prts-muted); }
|
|
972
|
+
& [data-prts-transfer-buttons] { display:flex; gap:8px; }
|
|
973
|
+
& [data-prts-transfer-recovery] { border:0; border-top:1px solid var(--prts-line); padding:16px 0 0; }
|
|
974
|
+
& [data-prts-transfer-status]:empty { display:none; }
|
|
975
|
+
}
|
|
976
|
+
@media(max-width:720px) {
|
|
977
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-body] { grid-template-rows:auto minmax(0,1fr) auto; }
|
|
978
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-sidebar] { display:contents; }
|
|
979
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-nav] { grid-row:1; }
|
|
980
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-theme-settings-scroll] { grid-row:2; }
|
|
981
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-actions] { grid-row:3; position:relative; flex-direction:row; align-items:center; justify-content:space-between; padding:12px 16px; border-top:1px solid var(--prts-line); background:var(--prts-canvas); }
|
|
982
|
+
html[data-dsh-prts] [data-prts-transfer-open] { width:auto; }
|
|
983
|
+
html[data-dsh-prts] [data-prts-theme-settings] :is([data-prts-reset-confirm],[data-prts-persistence-error]) { top:auto; bottom:100%; }
|
|
984
|
+
}
|
|
985
|
+
@media(max-width:540px) {
|
|
986
|
+
html[data-dsh-prts] [data-prts-transfer] { padding:20px; }
|
|
987
|
+
html[data-dsh-prts] [data-prts-transfer] [data-prts-transfer-columns] { grid-template-columns:1fr; gap:20px; }
|
|
988
|
+
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); }
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/* Shared quiet actions below the settings categories. */
|
|
992
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-settings-actions] {
|
|
993
|
+
padding:12px 10px; gap:2px; border-top:1px solid var(--prts-line);
|
|
994
|
+
}
|
|
995
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-footer-action] {
|
|
996
|
+
display:flex; align-items:center; justify-content:flex-start; gap:10px;
|
|
997
|
+
width:100%; min-height:40px; padding:10px; margin:0;
|
|
998
|
+
border:0; border-radius:5px; clip-path:none; box-shadow:none;
|
|
999
|
+
background:transparent; color:var(--prts-muted);
|
|
1000
|
+
font:500 12px/1.4 var(--prts-body); text-align:left; cursor:pointer;
|
|
1001
|
+
}
|
|
1002
|
+
html[data-dsh-prts] [data-prts-footer-action] svg {
|
|
1003
|
+
width:16px; height:16px; flex:none; fill:none; stroke:currentColor;
|
|
1004
|
+
stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round;
|
|
1005
|
+
}
|
|
1006
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-footer-action]:hover {
|
|
1007
|
+
background:var(--prts-panel-raised); color:var(--prts-ink);
|
|
1008
|
+
}
|
|
1009
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-footer-action]:focus-visible {
|
|
1010
|
+
outline:2px solid var(--prts-yellow); outline-offset:-2px;
|
|
1011
|
+
}
|
|
1012
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-footer-action][hidden] { display:none; }
|
|
1013
|
+
@media(max-width:720px) {
|
|
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; }
|
|
1015
|
+
html[data-dsh-prts] [data-prts-theme-settings] [data-prts-reset-zone] { width:100%; }
|
|
1016
|
+
}
|