@dsh-plus/image-studio 0.1.22 → 0.1.24
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/lib/client.js +680 -602
- package/package.json +2 -2
- package/src/client/card.tsx +142 -480
- package/src/client/client.ts +2 -2
- package/src/client/draft.ts +128 -0
- package/src/client/preset-sections.tsx +321 -0
- package/src/client/styles.ts +3 -4
- package/src/ns.ts +2 -2
package/src/client/client.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* 浏览器半入口:
|
|
3
3
|
* - sidebar.footer.action:侧栏底部入口按钮(与文件/终端入口 flex 等分行宽);
|
|
4
4
|
* - shell.overlay:图像工作室面板(文生图/图生图/画廊 + 任务条);
|
|
5
|
-
* - 配置卡片:injectPluginConfigCard
|
|
6
|
-
*
|
|
5
|
+
* - 配置卡片:injectPluginConfigCard 两槽位(三组预设 + 凭据 + 高级项;
|
|
6
|
+
* 插件页 row/bundle config)。
|
|
7
7
|
* 模式与 web-files 一致:CJS factory 产物、data-plugin-css 样式
|
|
8
8
|
* 约定、slot 失配静默降级;配置读写经 ctx.remote.settings 直连(远程域名可用)。
|
|
9
9
|
* @module @dsh-plus/image-studio/client
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置卡片草稿模型与草稿 ↔ settings 载荷转换(无 React/JSX,供 node --test 直接导入):
|
|
3
|
+
* JSON 字段(extraHeaders / paramSpecs)以文本镜像承载,保存时解析并做本地校验。
|
|
4
|
+
* @module image-studio/client/draft
|
|
5
|
+
*/
|
|
6
|
+
import type { ImageStudioConfig } from '../config.ts'
|
|
7
|
+
import { credentialRefNameOf, isValidPresetId } from '../credentials.ts'
|
|
8
|
+
import { validateParamSpecs } from '../params/spec.ts'
|
|
9
|
+
|
|
10
|
+
/** 编辑态草稿:JSON 字段与数值以文本承载(保存时解析校验)。 */
|
|
11
|
+
export interface ProviderDraft {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
protocol: string
|
|
15
|
+
baseUrl: string
|
|
16
|
+
model: string
|
|
17
|
+
extraHeadersText: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ParamDraft {
|
|
21
|
+
id: string
|
|
22
|
+
name: string
|
|
23
|
+
endpoint: string
|
|
24
|
+
specsText: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PromptDraft {
|
|
28
|
+
id: string
|
|
29
|
+
name: string
|
|
30
|
+
text: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Draft {
|
|
34
|
+
providers: ProviderDraft[]
|
|
35
|
+
params: ParamDraft[]
|
|
36
|
+
prompts: PromptDraft[]
|
|
37
|
+
maxConcurrent: string
|
|
38
|
+
requestTimeoutMs: string
|
|
39
|
+
proxy: string
|
|
40
|
+
galleryMaxItems: string
|
|
41
|
+
uploadTtlHours: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 配置值 → 编辑草稿。 */
|
|
45
|
+
export function draftOf(value: ImageStudioConfig): Draft {
|
|
46
|
+
return {
|
|
47
|
+
providers: value.providerPresets.map((p) => ({
|
|
48
|
+
id: p.id,
|
|
49
|
+
name: p.name,
|
|
50
|
+
protocol: p.protocol,
|
|
51
|
+
baseUrl: p.baseUrl,
|
|
52
|
+
model: p.model,
|
|
53
|
+
extraHeadersText: JSON.stringify(p.extraHeaders ?? {}, null, 2),
|
|
54
|
+
})),
|
|
55
|
+
params: value.paramPresets.map((p) => ({
|
|
56
|
+
id: p.id,
|
|
57
|
+
name: p.name,
|
|
58
|
+
endpoint: p.endpoint,
|
|
59
|
+
specsText: JSON.stringify(p.paramSpecs ?? {}, null, 2),
|
|
60
|
+
})),
|
|
61
|
+
prompts: value.promptPresets.map((p) => ({ id: p.id, name: p.name, text: p.text })),
|
|
62
|
+
maxConcurrent: String(value.maxConcurrent),
|
|
63
|
+
requestTimeoutMs: String(value.requestTimeoutMs),
|
|
64
|
+
proxy: value.proxy,
|
|
65
|
+
galleryMaxItems: String(value.galleryMaxItems),
|
|
66
|
+
uploadTtlHours: String(value.uploadTtlHours),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseJson(text: string, label: string): unknown {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(text)
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new Error(`${label} ${error instanceof Error ? error.message : String(error)}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 草稿 → settings 载荷(边界校验:JSON 解析、参数表校验、预设 id 形态)。 */
|
|
79
|
+
export function payloadOf(draft: Draft): Record<string, unknown> {
|
|
80
|
+
const providers = draft.providers.map((p) => {
|
|
81
|
+
if (!isValidPresetId(p.id)) throw new Error(p.id)
|
|
82
|
+
return {
|
|
83
|
+
id: p.id,
|
|
84
|
+
name: p.name,
|
|
85
|
+
protocol: p.protocol,
|
|
86
|
+
baseUrl: p.baseUrl,
|
|
87
|
+
model: p.model,
|
|
88
|
+
credentialRef: credentialRefNameOf(p.id),
|
|
89
|
+
extraHeaders: parseJson(p.extraHeadersText, p.id) as Record<string, string>,
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
const params = draft.params.map((p) => ({
|
|
93
|
+
id: p.id,
|
|
94
|
+
name: p.name,
|
|
95
|
+
endpoint: p.endpoint,
|
|
96
|
+
paramSpecs: validateParamSpecs(parseJson(p.specsText, p.id)),
|
|
97
|
+
}))
|
|
98
|
+
return {
|
|
99
|
+
promptPresets: draft.prompts.map((p) => ({ id: p.id, name: p.name, text: p.text })),
|
|
100
|
+
paramPresets: params,
|
|
101
|
+
providerPresets: providers,
|
|
102
|
+
maxConcurrent: Math.max(0, Math.floor(Number(draft.maxConcurrent) || 0)),
|
|
103
|
+
requestTimeoutMs: Math.max(10_000, Math.floor(Number(draft.requestTimeoutMs) || 300_000)),
|
|
104
|
+
proxy: draft.proxy,
|
|
105
|
+
galleryMaxItems: Math.max(0, Math.floor(Number(draft.galleryMaxItems) || 0)),
|
|
106
|
+
uploadTtlHours: Math.max(1, Math.floor(Number(draft.uploadTtlHours) || 24)),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 空提供商预设(新增行用;协议取目录首项)。 */
|
|
111
|
+
export function emptyProviderDraft(protocol: string): ProviderDraft {
|
|
112
|
+
return { id: '', name: '', protocol, baseUrl: '', model: '', extraHeadersText: '{}' }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 空参数预设(新增行用;id 由时间戳生成,行序即身份)。 */
|
|
116
|
+
export function emptyParamDraft(): ParamDraft {
|
|
117
|
+
return {
|
|
118
|
+
id: `param-${Date.now().toString(36)}`,
|
|
119
|
+
name: '',
|
|
120
|
+
endpoint: 'generation',
|
|
121
|
+
specsText: '{}',
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 空提示词预设(新增行用)。 */
|
|
126
|
+
export function emptyPromptDraft(): PromptDraft {
|
|
127
|
+
return { id: `prompt-${Date.now().toString(36)}`, name: '', text: '' }
|
|
128
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置卡片的三组预设编辑区:提供商(含逐条 API Key 管理)、提示词、参数。
|
|
3
|
+
* 从 card.tsx 拆出,避免单组件过长;草稿类型与纯转换在 ./draft.ts。
|
|
4
|
+
* @module image-studio/client/preset-sections
|
|
5
|
+
*/
|
|
6
|
+
import { TextField } from '@dsh-plus/shared/client'
|
|
7
|
+
import { type ReactElement, useEffect, useState } from 'react'
|
|
8
|
+
|
|
9
|
+
import { isValidPresetId } from '../credentials.ts'
|
|
10
|
+
import { fetchCredentialStatus, setCredential, unsetCredential } from './api.ts'
|
|
11
|
+
import {
|
|
12
|
+
type Draft,
|
|
13
|
+
emptyParamDraft,
|
|
14
|
+
emptyPromptDraft,
|
|
15
|
+
emptyProviderDraft,
|
|
16
|
+
type ParamDraft,
|
|
17
|
+
type PromptDraft,
|
|
18
|
+
type ProviderDraft,
|
|
19
|
+
} from './draft.ts'
|
|
20
|
+
import type { Translate } from './i18n.ts'
|
|
21
|
+
|
|
22
|
+
/** 单个提供商预设的 API Key 行(describe 徽标 + 设置/删除,值不回显)。 */
|
|
23
|
+
function CredentialRow(props: { t: Translate; presetId: string; disabled: boolean }): ReactElement {
|
|
24
|
+
const { t, presetId, disabled } = props
|
|
25
|
+
const [configured, setConfigured] = useState<boolean | null>(null)
|
|
26
|
+
const [value, setValue] = useState('')
|
|
27
|
+
const [busy, setBusy] = useState(false)
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!isValidPresetId(presetId)) return
|
|
31
|
+
let alive = true
|
|
32
|
+
fetchCredentialStatus(presetId)
|
|
33
|
+
.then((res) => {
|
|
34
|
+
if (alive) setConfigured(res.configured)
|
|
35
|
+
})
|
|
36
|
+
.catch(() => {})
|
|
37
|
+
return () => {
|
|
38
|
+
alive = false
|
|
39
|
+
}
|
|
40
|
+
}, [presetId])
|
|
41
|
+
|
|
42
|
+
const act = (fn: () => Promise<unknown>): void => {
|
|
43
|
+
setBusy(true)
|
|
44
|
+
fn()
|
|
45
|
+
.then(() => fetchCredentialStatus(presetId))
|
|
46
|
+
.then((res) => {
|
|
47
|
+
setConfigured(res.configured)
|
|
48
|
+
setValue('')
|
|
49
|
+
})
|
|
50
|
+
.catch(() => {})
|
|
51
|
+
.finally(() => setBusy(false))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="imsc-credRow">
|
|
56
|
+
<input
|
|
57
|
+
className="imsc-input"
|
|
58
|
+
type="password"
|
|
59
|
+
autoComplete="off"
|
|
60
|
+
placeholder={t('card.credential.placeholder')}
|
|
61
|
+
aria-label={t('card.credential.label')}
|
|
62
|
+
value={value}
|
|
63
|
+
disabled={disabled || busy}
|
|
64
|
+
onChange={(event) => setValue(event.target.value)}
|
|
65
|
+
/>
|
|
66
|
+
{configured !== null ? (
|
|
67
|
+
<span className={`imsc-badge ${configured ? 'imsc-badgeSet' : 'imsc-badgeUnset'}`}>
|
|
68
|
+
{t(configured ? 'card.credential.configured' : 'card.credential.unconfigured')}
|
|
69
|
+
</span>
|
|
70
|
+
) : null}
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
className="imsc-btn imsc-btnGhost"
|
|
74
|
+
disabled={disabled || busy || value === '' || !isValidPresetId(presetId)}
|
|
75
|
+
onClick={() => act(() => setCredential(presetId, value))}
|
|
76
|
+
>
|
|
77
|
+
{t('card.credential.set')}
|
|
78
|
+
</button>
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
className="imsc-btn imsc-btnGhost"
|
|
82
|
+
disabled={disabled || busy || configured !== true || !isValidPresetId(presetId)}
|
|
83
|
+
onClick={() => act(() => unsetCredential(presetId))}
|
|
84
|
+
>
|
|
85
|
+
{t('card.credential.unset')}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 三组预设编辑区共用的 props(各组按需取用其中一部分)。 */
|
|
92
|
+
export interface PresetSectionsProps {
|
|
93
|
+
t: Translate
|
|
94
|
+
draft: Draft
|
|
95
|
+
disabled: boolean
|
|
96
|
+
protocols: Array<{ id: string; label: string }>
|
|
97
|
+
onPatch(patch: Partial<Draft>): void
|
|
98
|
+
onPatchProvider(index: number, patch: Partial<ProviderDraft>): void
|
|
99
|
+
onPatchParam(index: number, patch: Partial<ParamDraft>): void
|
|
100
|
+
onPatchPrompt(index: number, patch: Partial<PromptDraft>): void
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function ProviderPresetSection(props: PresetSectionsProps): ReactElement {
|
|
104
|
+
const { t, draft, disabled, protocols, onPatch, onPatchProvider } = props
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
<h4 className="imsc-groupTitle">{t('card.providers')}</h4>
|
|
108
|
+
{draft.providers.map((provider, index) => (
|
|
109
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 草稿行无稳定 id(id 字段本身可编辑),行序即身份
|
|
110
|
+
<div className="imsc-presetBox" key={`provider-${index}`}>
|
|
111
|
+
<div className="imsc-presetHead">
|
|
112
|
+
<span className="imsc-presetName">{provider.name || provider.id || '—'}</span>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className="imsc-btn imsc-btnGhost"
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
onClick={() => onPatch({ providers: draft.providers.filter((_, i) => i !== index) })}
|
|
118
|
+
>
|
|
119
|
+
{t('common.delete')}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="imsc-grid2">
|
|
123
|
+
<TextField
|
|
124
|
+
prefix="imsc"
|
|
125
|
+
id={`imsc-pid-${index}`}
|
|
126
|
+
label={t('card.field.id')}
|
|
127
|
+
hint={t('card.idHint')}
|
|
128
|
+
value={provider.id}
|
|
129
|
+
invalid={provider.id !== '' && !isValidPresetId(provider.id)}
|
|
130
|
+
invalidLabel={t('card.idInvalid')}
|
|
131
|
+
disabled={disabled}
|
|
132
|
+
onEdit={(v) => onPatchProvider(index, { id: v })}
|
|
133
|
+
/>
|
|
134
|
+
<TextField
|
|
135
|
+
prefix="imsc"
|
|
136
|
+
id={`imsc-pname-${index}`}
|
|
137
|
+
label={t('card.field.name')}
|
|
138
|
+
value={provider.name}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
onEdit={(v) => onPatchProvider(index, { name: v })}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="imsc-grid2">
|
|
144
|
+
<div className="imsc-field">
|
|
145
|
+
<div className="imsc-head">
|
|
146
|
+
<label className="imsc-label" htmlFor={`imsc-pproto-${index}`}>
|
|
147
|
+
{t('card.field.protocol')}
|
|
148
|
+
</label>
|
|
149
|
+
</div>
|
|
150
|
+
<select
|
|
151
|
+
id={`imsc-pproto-${index}`}
|
|
152
|
+
className="imsc-select"
|
|
153
|
+
value={provider.protocol}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
onChange={(event) => onPatchProvider(index, { protocol: event.target.value })}
|
|
156
|
+
>
|
|
157
|
+
{(protocols.length > 0
|
|
158
|
+
? protocols
|
|
159
|
+
: [{ id: provider.protocol, label: provider.protocol }]
|
|
160
|
+
).map((protocol) => (
|
|
161
|
+
<option key={protocol.id} value={protocol.id}>
|
|
162
|
+
{protocol.label}
|
|
163
|
+
</option>
|
|
164
|
+
))}
|
|
165
|
+
</select>
|
|
166
|
+
</div>
|
|
167
|
+
<TextField
|
|
168
|
+
prefix="imsc"
|
|
169
|
+
id={`imsc-pmodel-${index}`}
|
|
170
|
+
label={t('card.field.model')}
|
|
171
|
+
value={provider.model}
|
|
172
|
+
disabled={disabled}
|
|
173
|
+
onEdit={(v) => onPatchProvider(index, { model: v })}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
<TextField
|
|
177
|
+
prefix="imsc"
|
|
178
|
+
id={`imsc-purl-${index}`}
|
|
179
|
+
label={t('card.field.baseUrl')}
|
|
180
|
+
value={provider.baseUrl}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
onEdit={(v) => onPatchProvider(index, { baseUrl: v })}
|
|
183
|
+
/>
|
|
184
|
+
<div className="imsc-field">
|
|
185
|
+
<div className="imsc-head">
|
|
186
|
+
<label className="imsc-label" htmlFor={`imsc-pheaders-${index}`}>
|
|
187
|
+
{t('card.field.extraHeaders')}
|
|
188
|
+
</label>
|
|
189
|
+
</div>
|
|
190
|
+
<textarea
|
|
191
|
+
id={`imsc-pheaders-${index}`}
|
|
192
|
+
className="imsc-textarea"
|
|
193
|
+
value={provider.extraHeadersText}
|
|
194
|
+
disabled={disabled}
|
|
195
|
+
onChange={(event) => onPatchProvider(index, { extraHeadersText: event.target.value })}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
<CredentialRow t={t} presetId={provider.id} disabled={disabled} />
|
|
199
|
+
</div>
|
|
200
|
+
))}
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
className="imsc-btn imsc-btnGhost"
|
|
204
|
+
disabled={disabled}
|
|
205
|
+
onClick={() =>
|
|
206
|
+
onPatch({
|
|
207
|
+
providers: [
|
|
208
|
+
...draft.providers,
|
|
209
|
+
emptyProviderDraft(protocols[0]?.id ?? 'openai-images'),
|
|
210
|
+
],
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
>
|
|
214
|
+
{t('card.add')} · {t('card.providers')}
|
|
215
|
+
</button>
|
|
216
|
+
</>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function PromptPresetSection(props: PresetSectionsProps): ReactElement {
|
|
221
|
+
const { t, draft, disabled, onPatch, onPatchPrompt } = props
|
|
222
|
+
return (
|
|
223
|
+
<>
|
|
224
|
+
<h4 className="imsc-groupTitle">{t('card.prompts')}</h4>
|
|
225
|
+
{draft.prompts.map((prompt, index) => (
|
|
226
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 草稿行无稳定 id,行序即身份
|
|
227
|
+
<div className="imsc-presetBox" key={`prompt-${index}`}>
|
|
228
|
+
<div className="imsc-presetHead">
|
|
229
|
+
<input
|
|
230
|
+
className="imsc-input"
|
|
231
|
+
aria-label={t('card.field.name')}
|
|
232
|
+
value={prompt.name}
|
|
233
|
+
disabled={disabled}
|
|
234
|
+
onChange={(event) => onPatchPrompt(index, { name: event.target.value })}
|
|
235
|
+
/>
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
className="imsc-btn imsc-btnGhost"
|
|
239
|
+
disabled={disabled}
|
|
240
|
+
onClick={() => onPatch({ prompts: draft.prompts.filter((_, i) => i !== index) })}
|
|
241
|
+
>
|
|
242
|
+
{t('common.delete')}
|
|
243
|
+
</button>
|
|
244
|
+
</div>
|
|
245
|
+
<textarea
|
|
246
|
+
className="imsc-textarea"
|
|
247
|
+
aria-label={t('card.field.text')}
|
|
248
|
+
value={prompt.text}
|
|
249
|
+
disabled={disabled}
|
|
250
|
+
onChange={(event) => onPatchPrompt(index, { text: event.target.value })}
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
))}
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
className="imsc-btn imsc-btnGhost"
|
|
257
|
+
disabled={disabled}
|
|
258
|
+
onClick={() => onPatch({ prompts: [...draft.prompts, emptyPromptDraft()] })}
|
|
259
|
+
>
|
|
260
|
+
{t('card.add')} · {t('card.prompts')}
|
|
261
|
+
</button>
|
|
262
|
+
</>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function ParamPresetSection(props: PresetSectionsProps): ReactElement {
|
|
267
|
+
const { t, draft, disabled, onPatch, onPatchParam } = props
|
|
268
|
+
return (
|
|
269
|
+
<>
|
|
270
|
+
<h4 className="imsc-groupTitle">{t('card.params')}</h4>
|
|
271
|
+
{draft.params.map((param, index) => (
|
|
272
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 草稿行无稳定 id,行序即身份
|
|
273
|
+
<div className="imsc-presetBox" key={`param-${index}`}>
|
|
274
|
+
<div className="imsc-presetHead">
|
|
275
|
+
<input
|
|
276
|
+
className="imsc-input"
|
|
277
|
+
aria-label={t('card.field.name')}
|
|
278
|
+
value={param.name}
|
|
279
|
+
disabled={disabled}
|
|
280
|
+
onChange={(event) => onPatchParam(index, { name: event.target.value })}
|
|
281
|
+
/>
|
|
282
|
+
<select
|
|
283
|
+
className="imsc-select"
|
|
284
|
+
aria-label={t('card.field.endpoint')}
|
|
285
|
+
value={param.endpoint}
|
|
286
|
+
disabled={disabled}
|
|
287
|
+
onChange={(event) => onPatchParam(index, { endpoint: event.target.value })}
|
|
288
|
+
>
|
|
289
|
+
<option value="generation">{t('tab.generation')}</option>
|
|
290
|
+
<option value="edit">{t('tab.edit')}</option>
|
|
291
|
+
</select>
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
className="imsc-btn imsc-btnGhost"
|
|
295
|
+
disabled={disabled}
|
|
296
|
+
onClick={() => onPatch({ params: draft.params.filter((_, i) => i !== index) })}
|
|
297
|
+
>
|
|
298
|
+
{t('common.delete')}
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
<textarea
|
|
302
|
+
className="imsc-textarea"
|
|
303
|
+
aria-label={t('card.field.paramSpecs')}
|
|
304
|
+
placeholder='{ "size": { "enabled": true, "value": "1024x1024" } }'
|
|
305
|
+
value={param.specsText}
|
|
306
|
+
disabled={disabled}
|
|
307
|
+
onChange={(event) => onPatchParam(index, { specsText: event.target.value })}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
))}
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
className="imsc-btn imsc-btnGhost"
|
|
314
|
+
disabled={disabled}
|
|
315
|
+
onClick={() => onPatch({ params: [...draft.params, emptyParamDraft()] })}
|
|
316
|
+
>
|
|
317
|
+
{t('card.add')} · {t('card.params')}
|
|
318
|
+
</button>
|
|
319
|
+
</>
|
|
320
|
+
)
|
|
321
|
+
}
|
package/src/client/styles.ts
CHANGED
|
@@ -561,10 +561,8 @@ export const imageStudioCss = `
|
|
|
561
561
|
.imsc-loading{padding:14px 16px;margin:0;font-size:13px;color:var(--dsw-alias-label-tertiary)}
|
|
562
562
|
.imsc-groupTitle { margin: 14px 0 6px; font-size: 13px; font-weight: 600; }
|
|
563
563
|
.imsc-presetBox {
|
|
564
|
-
border: 1px solid var(--dsw-alias-border-l3);
|
|
565
|
-
|
|
566
|
-
padding: 10px;
|
|
567
|
-
margin-bottom: 10px;
|
|
564
|
+
border-top: 1px solid var(--dsw-alias-border-l3);
|
|
565
|
+
padding: 10px 0 2px;
|
|
568
566
|
display: grid;
|
|
569
567
|
gap: 8px;
|
|
570
568
|
}
|
|
@@ -587,6 +585,7 @@ export const imageStudioCss = `
|
|
|
587
585
|
.imsc-credRow { display: flex; align-items: center; gap: 8px; }
|
|
588
586
|
.imsc-credRow .imsc-input { flex: 1; min-width: 0; }
|
|
589
587
|
|
|
588
|
+
|
|
590
589
|
/* ── 移动端(≤767px,与 ui-mobile-fit 同断点)── */
|
|
591
590
|
@media (max-width: 767px) {
|
|
592
591
|
.ims-modal {
|
package/src/ns.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
|
|
3
3
|
* 服务端直接以字面量注册(0.1.2-alpha.2 起 dsh-settings 对命名空间做编译期
|
|
4
4
|
* 语法校验,见 config.ts);浏览器半把它作为配置卡片的注册键
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* (插件页配置卡片挂在 row/bundle config 槽位上,与命名空间解耦,
|
|
6
|
+
* 见 shared/config-slots),
|
|
7
7
|
* 同一字面量两处共用,防止漂移。
|
|
8
8
|
* @module image-studio/ns
|
|
9
9
|
*/
|