收口创作入口契约后台表单

将统一创作契约泥点消耗改为数字字段并由前端格式化展示
将后台契约编辑从 JSON 文本改为结构化卡片与弹窗表单
隐藏玩法阶段等内部标识并按玩法默认映射自动带出
更新创作入口文档、团队记忆和回归测试
This commit is contained in:
2026-06-07 23:53:26 +08:00
parent 2a6da01307
commit 17662916cd
13 changed files with 822 additions and 108 deletions

View File

@@ -205,6 +205,7 @@ export interface AdminUpsertCreationEntryEventBannersRequest {
export interface UnifiedCreationSpecPayload {
playId: string;
title: string;
mudPointCost: number;
workspaceStage: string;
generationStage: string;
resultStage: string;

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
import {fireEvent, render, screen, waitFor, within} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {beforeEach, expect, test, vi} from 'vitest';
@@ -28,6 +28,7 @@ vi.mock('../api/adminApiClient', () => ({
const puzzleSpec: UnifiedCreationSpecPayload = {
playId: 'puzzle',
title: '拼图',
mudPointCost: 10,
workspaceStage: 'puzzle-agent-workspace',
generationStage: 'puzzle-generating',
resultStage: 'puzzle-result',
@@ -93,6 +94,22 @@ test('创作入口后台展示并保存统一创作契约', async () => {
).toContain('拼图');
expect(container.querySelector('.admin-panel .admin-panel')).toBeNull();
expect(container.querySelector('.admin-muted')).toBeNull();
expect(screen.queryByLabelText('契约 JSON')).toBeNull();
expect(screen.queryByText('puzzle-generating')).toBeNull();
await user.click(screen.getByRole('button', {name: '修改契约'}));
const dialog = screen.getByRole('dialog', {name: '统一创作契约'});
expect(within(dialog).queryByLabelText('玩法 ID')).toBeNull();
expect(within(dialog).queryByLabelText('工作台阶段')).toBeNull();
expect(within(dialog).queryByLabelText('生成阶段')).toBeNull();
expect(within(dialog).queryByLabelText('结果阶段')).toBeNull();
fireEvent.change(within(dialog).getByLabelText('泥点消耗'), {
target: {value: '12'},
});
await user.click(within(dialog).getByRole('button', {name: '应用修改'}));
expect(screen.queryByRole('dialog', {name: '统一创作契约'})).toBeNull();
expect(screen.getByText('12泥点数')).toBeTruthy();
await user.click(screen.getByRole('button', {name: '保存入库'}));
await user.click(screen.getByRole('button', {name: '确认'}));
@@ -102,7 +119,10 @@ test('创作入口后台展示并保存统一创作契约', async () => {
'admin-token',
expect.objectContaining({
id: 'puzzle',
unifiedCreationSpec: puzzleSpec,
unifiedCreationSpec: {
...puzzleSpec,
mudPointCost: 12,
},
}),
);
});
@@ -110,6 +130,18 @@ test('创作入口后台展示并保存统一创作契约', async () => {
test('创作入口后台拒绝 playId 不一致的统一创作契约', async () => {
const user = userEvent.setup();
vi.mocked(getAdminCreationEntryConfig).mockResolvedValueOnce({
...configResponse,
entries: [
{
...configResponse.entries[0]!,
unifiedCreationSpec: {
...puzzleSpec,
playId: 'match3d',
},
},
],
});
render(
<AdminCreationEntrySwitchPage
token="admin-token"
@@ -117,15 +149,7 @@ test('创作入口后台拒绝 playId 不一致的统一创作契约', async ()
/>,
);
const textarea = await screen.findByLabelText('契约 JSON');
fireEvent.change(textarea, {
target: {
value: JSON.stringify({
...puzzleSpec,
playId: 'match3d',
}),
},
});
await screen.findByText('pictureDescription');
await user.click(screen.getByRole('button', {name: '保存入库'}));
expect(await screen.findByText('统一创作契约 playId 必须与入口 ID 一致')).toBeTruthy();

View File

@@ -1,4 +1,4 @@
import { Plus, RefreshCcw, Save, Trash2 } from 'lucide-react';
import { Pencil, Plus, RefreshCcw, Save, Trash2, X } from 'lucide-react';
import { FormEvent, useEffect, useState } from 'react';
import {
@@ -34,7 +34,100 @@ type AnnouncementFormBuildResult =
| { ok: true; json: string }
| { ok: false; message: string };
/** 统一创作契约字段的弹窗表单态。 */
type UnifiedCreationSpecFieldFormItem = {
id: string;
fieldId: string;
kind: UnifiedCreationFieldPayload['kind'];
label: string;
required: boolean;
};
/** 统一创作契约弹窗表单态;保存入库前会重新组装为后端契约。 */
type UnifiedCreationSpecFormState = {
playId: string;
title: string;
mudPointCost: string;
/** 内部阶段由已有契约或玩法默认映射带出,不在后台表单中开放编辑。 */
workspaceStage: string;
generationStage: string;
resultStage: string;
fields: UnifiedCreationSpecFieldFormItem[];
};
type UnifiedCreationSpecStageState = Pick<
UnifiedCreationSpecFormState,
'workspaceStage' | 'generationStage' | 'resultStage'
>;
const DEFAULT_UNIFIED_CREATION_STAGE_MAP: Record<
string,
UnifiedCreationSpecStageState
> = {
rpg: {
workspaceStage: 'agent-workspace',
generationStage: 'custom-world-generating',
resultStage: 'custom-world-result',
},
'big-fish': {
workspaceStage: 'big-fish-agent-workspace',
generationStage: 'big-fish-generating',
resultStage: 'big-fish-result',
},
puzzle: {
workspaceStage: 'puzzle-agent-workspace',
generationStage: 'puzzle-generating',
resultStage: 'puzzle-result',
},
'puzzle-clear': {
workspaceStage: 'puzzle-clear-workspace',
generationStage: 'puzzle-clear-generating',
resultStage: 'puzzle-clear-result',
},
match3d: {
workspaceStage: 'match3d-agent-workspace',
generationStage: 'match3d-generating',
resultStage: 'match3d-result',
},
'jump-hop': {
workspaceStage: 'jump-hop-workspace',
generationStage: 'jump-hop-generating',
resultStage: 'jump-hop-result',
},
'wooden-fish': {
workspaceStage: 'wooden-fish-workspace',
generationStage: 'wooden-fish-generating',
resultStage: 'wooden-fish-result',
},
'square-hole': {
workspaceStage: 'square-hole-agent-workspace',
generationStage: 'square-hole-generating',
resultStage: 'square-hole-result',
},
'bark-battle': {
workspaceStage: 'bark-battle-workspace',
generationStage: 'bark-battle-generating',
resultStage: 'bark-battle-result',
},
'visual-novel': {
workspaceStage: 'visual-novel-agent-workspace',
generationStage: 'visual-novel-generating',
resultStage: 'visual-novel-result',
},
'baby-object-match': {
workspaceStage: 'baby-object-match-workspace',
generationStage: 'baby-object-match-generating',
resultStage: 'baby-object-match-result',
},
'creative-agent': {
workspaceStage: 'creative-agent-workspace',
generationStage: 'puzzle-generating',
resultStage: 'puzzle-result',
},
};
let announcementFormItemSequence = 0;
let unifiedCreationSpecFieldSequence = 0;
export function AdminCreationEntrySwitchPage({
token,
@@ -55,7 +148,12 @@ export function AdminCreationEntrySwitchPage({
const [categoryId, setCategoryId] = useState('recommended');
const [categoryLabel, setCategoryLabel] = useState('热门推荐');
const [categorySortOrder, setCategorySortOrder] = useState('20');
const [unifiedCreationSpecJson, setUnifiedCreationSpecJson] = useState('');
const [unifiedCreationSpec, setUnifiedCreationSpec] =
useState<UnifiedCreationSpecPayload | null>(null);
const [unifiedCreationSpecForm, setUnifiedCreationSpecForm] =
useState<UnifiedCreationSpecFormState | null>(null);
const [unifiedCreationSpecFormError, setUnifiedCreationSpecFormError] =
useState('');
const [announcementItems, setAnnouncementItems] = useState<
AnnouncementFormItem[]
>([]);
@@ -101,9 +199,9 @@ export function AdminCreationEntrySwitchPage({
const targetId = selectedId.trim();
setErrorMessage('');
const unifiedCreationSpecResult = parseUnifiedCreationSpecJson(
const unifiedCreationSpecResult = validateUnifiedCreationSpecForEntry(
targetId,
unifiedCreationSpecJson,
unifiedCreationSpec,
);
if (!unifiedCreationSpecResult.ok) {
setErrorMessage(unifiedCreationSpecResult.message);
@@ -193,9 +291,95 @@ export function AdminCreationEntrySwitchPage({
setCategoryId(entry.categoryId);
setCategoryLabel(entry.categoryLabel);
setCategorySortOrder(String(entry.categorySortOrder));
setUnifiedCreationSpecJson(
formatUnifiedCreationSpecJson(entry.unifiedCreationSpec),
setUnifiedCreationSpec(entry.unifiedCreationSpec ?? null);
setUnifiedCreationSpecForm(null);
setUnifiedCreationSpecFormError('');
}
/** 打开统一创作契约弹窗;缺省时用当前入口 ID 和标题预填。 */
function openUnifiedCreationSpecForm() {
setUnifiedCreationSpecForm(
buildUnifiedCreationSpecForm(
unifiedCreationSpec,
selectedId.trim(),
title.trim(),
),
);
setUnifiedCreationSpecFormError('');
}
function closeUnifiedCreationSpecForm() {
setUnifiedCreationSpecForm(null);
setUnifiedCreationSpecFormError('');
}
function applyUnifiedCreationSpecForm(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!unifiedCreationSpecForm) {
return;
}
const result = buildUnifiedCreationSpecFromForm(unifiedCreationSpecForm);
if (!result.ok) {
setUnifiedCreationSpecFormError(result.message);
return;
}
if (result.spec.playId !== selectedId.trim()) {
setUnifiedCreationSpecFormError('统一创作契约 playId 必须与入口 ID 一致');
return;
}
setUnifiedCreationSpec(result.spec);
closeUnifiedCreationSpecForm();
}
function updateUnifiedCreationSpecForm(
patch: Partial<Omit<UnifiedCreationSpecFormState, 'fields'>>,
) {
setUnifiedCreationSpecForm((currentForm) =>
currentForm ? { ...currentForm, ...patch } : currentForm,
);
}
function updateUnifiedCreationSpecField(
index: number,
patch: Partial<Omit<UnifiedCreationSpecFieldFormItem, 'id'>>,
) {
setUnifiedCreationSpecForm((currentForm) =>
currentForm
? {
...currentForm,
fields: currentForm.fields.map((field, fieldIndex) =>
fieldIndex === index ? { ...field, ...patch } : field,
),
}
: currentForm,
);
}
function addUnifiedCreationSpecField() {
setUnifiedCreationSpecForm((currentForm) =>
currentForm
? {
...currentForm,
fields: [...currentForm.fields, createUnifiedCreationSpecFieldFormItem()],
}
: currentForm,
);
}
function removeUnifiedCreationSpecField(index: number) {
setUnifiedCreationSpecForm((currentForm) => {
if (!currentForm) {
return currentForm;
}
const fields = currentForm.fields.filter(
(_, fieldIndex) => fieldIndex !== index,
);
return {
...currentForm,
fields: fields.length > 0 ? fields : [createUnifiedCreationSpecFieldFormItem()],
};
});
}
/** 更新单条公告表单字段,避免后台页面直接暴露 JSON 编辑。 */
@@ -431,24 +615,34 @@ export function AdminCreationEntrySwitchPage({
<div className="admin-subsection-heading">
<span></span>
<span>
{unifiedCreationSpecJson.trim() ? '已配置' : '未配置'}
{unifiedCreationSpec ? '已配置' : '未配置'}
</span>
</div>
{unifiedCreationSpecJson.trim() ? (
<UnifiedCreationSpecSummary specJson={unifiedCreationSpecJson} />
<div className="admin-form-actions">
<button
className="admin-secondary-button"
type="button"
onClick={openUnifiedCreationSpecForm}
>
<Pencil size={17} aria-hidden="true" />
<span>{unifiedCreationSpec ? '修改契约' : '新增契约'}</span>
</button>
{unifiedCreationSpec ? (
<button
className="admin-secondary-button"
type="button"
onClick={() => setUnifiedCreationSpec(null)}
>
<Trash2 size={17} aria-hidden="true" />
<span></span>
</button>
) : null}
</div>
{unifiedCreationSpec ? (
<UnifiedCreationSpecCard spec={unifiedCreationSpec} />
) : (
<div className="admin-muted-text"></div>
)}
<label className="admin-field">
<span> JSON</span>
<textarea
rows={12}
value={unifiedCreationSpecJson}
onChange={(event) =>
setUnifiedCreationSpecJson(event.target.value)
}
/>
</label>
</section>
{errorMessage ? (
@@ -509,6 +703,171 @@ export function AdminCreationEntrySwitchPage({
) : null}
{confirmDialog}
{unifiedCreationSpecForm ? (
<div
className="admin-confirm-backdrop"
role="presentation"
onMouseDown={(event) => {
if (event.target === event.currentTarget) {
closeUnifiedCreationSpecForm();
}
}}
>
<form
aria-labelledby="admin-unified-creation-spec-editor-title"
aria-modal="true"
className="admin-detail-panel admin-form admin-contract-dialog"
role="dialog"
onSubmit={applyUnifiedCreationSpecForm}
>
<div className="admin-panel-heading">
<h3 id="admin-unified-creation-spec-editor-title">
</h3>
<button
className="admin-ghost-button"
title="关闭"
type="button"
onClick={closeUnifiedCreationSpecForm}
>
<X size={17} aria-hidden="true" />
</button>
</div>
<label className="admin-field">
<span></span>
<input
value={unifiedCreationSpecForm.title}
onChange={(event) =>
updateUnifiedCreationSpecForm({
title: event.target.value,
})
}
/>
</label>
<label className="admin-field">
<span></span>
<input
inputMode="numeric"
min={1}
step={1}
type="number"
value={unifiedCreationSpecForm.mudPointCost}
onChange={(event) =>
updateUnifiedCreationSpecForm({
mudPointCost: event.target.value,
})
}
/>
</label>
<section className="admin-subsection">
<div className="admin-subsection-heading">
<span></span>
<button
className="admin-link-button"
type="button"
onClick={addUnifiedCreationSpecField}
>
<Plus size={15} aria-hidden="true" />
</button>
</div>
<div className="admin-contract-field-editor-list">
{unifiedCreationSpecForm.fields.map((field, index) => (
<section className="admin-contract-field-editor" key={field.id}>
<div className="admin-subsection-heading">
<span>{`字段 ${index + 1}`}</span>
<button
className="admin-link-button"
type="button"
aria-label={`删除字段 ${index + 1}`}
onClick={() => removeUnifiedCreationSpecField(index)}
>
<Trash2 size={15} aria-hidden="true" />
</button>
</div>
<div className="admin-contract-field-editor-grid">
<label className="admin-field">
<span>{`字段 ${index + 1} ID`}</span>
<input
value={field.fieldId}
onChange={(event) =>
updateUnifiedCreationSpecField(index, {
fieldId: event.target.value,
})
}
/>
</label>
<label className="admin-field">
<span>{`字段 ${index + 1} 类型`}</span>
<select
value={field.kind}
onChange={(event) =>
updateUnifiedCreationSpecField(index, {
kind: event.target
.value as UnifiedCreationFieldPayload['kind'],
})
}
>
<option value="text">text</option>
<option value="select">select</option>
<option value="image">image</option>
<option value="audio">audio</option>
</select>
</label>
<label className="admin-field">
<span>{`字段 ${index + 1} 标签`}</span>
<input
value={field.label}
onChange={(event) =>
updateUnifiedCreationSpecField(index, {
label: event.target.value,
})
}
/>
</label>
<label className="admin-switch-field admin-contract-required-toggle">
<input
checked={field.required}
type="checkbox"
onChange={(event) =>
updateUnifiedCreationSpecField(index, {
required: event.target.checked,
})
}
/>
<span></span>
</label>
</div>
</section>
))}
</div>
</section>
{unifiedCreationSpecFormError ? (
<div className="admin-alert" role="status">
{unifiedCreationSpecFormError}
</div>
) : null}
<div className="admin-confirm-actions">
<button
className="admin-secondary-button"
type="button"
onClick={closeUnifiedCreationSpecForm}
>
</button>
<button className="admin-primary-button" type="submit">
</button>
</div>
</form>
</div>
) : null}
</section>
);
}
@@ -607,21 +966,100 @@ function escapeAnnouncementHtmlText(value: string): string {
.replaceAll('"', '&quot;');
}
function formatUnifiedCreationSpecJson(
spec: UnifiedCreationSpecPayload | null | undefined,
) {
return spec ? JSON.stringify(spec, null, 2) : '';
function nextUnifiedCreationSpecFieldFormItemId() {
unifiedCreationSpecFieldSequence += 1;
return `unified-field-${unifiedCreationSpecFieldSequence}`;
}
function parseUnifiedCreationSpecJson(entryId: string, value: string) {
const parsed = parseUnifiedCreationSpecSummaryJson(value);
if (!parsed.ok || !parsed.spec) {
return parsed;
function createUnifiedCreationSpecFieldFormItem(
field?: UnifiedCreationFieldPayload,
): UnifiedCreationSpecFieldFormItem {
return {
id: nextUnifiedCreationSpecFieldFormItemId(),
fieldId: field?.id ?? '',
kind: field?.kind ?? 'text',
label: field?.label ?? '',
required: field?.required ?? true,
};
}
function buildUnifiedCreationSpecForm(
spec: UnifiedCreationSpecPayload | null,
entryId: string,
entryTitle: string,
): UnifiedCreationSpecFormState {
const playId = spec?.playId ?? entryId;
const stages = resolveUnifiedCreationSpecStages(spec, playId);
return {
playId,
title: spec?.title ?? entryTitle,
mudPointCost: String(normalizeMudPointCost(spec?.mudPointCost)),
...stages,
fields:
spec?.fields.map((field) => createUnifiedCreationSpecFieldFormItem(field)) ??
[createUnifiedCreationSpecFieldFormItem()],
};
}
function resolveUnifiedCreationSpecStages(
spec: UnifiedCreationSpecPayload | null,
playId: string,
): UnifiedCreationSpecStageState {
if (spec?.workspaceStage && spec.generationStage && spec.resultStage) {
return {
workspaceStage: spec.workspaceStage,
generationStage: spec.generationStage,
resultStage: spec.resultStage,
};
}
if (parsed.spec.playId !== entryId) {
return (
DEFAULT_UNIFIED_CREATION_STAGE_MAP[playId] ?? {
workspaceStage: '',
generationStage: '',
resultStage: '',
}
);
}
function buildUnifiedCreationSpecFromForm(form: UnifiedCreationSpecFormState) {
return validateUnifiedCreationSpec({
playId: form.playId,
title: form.title,
mudPointCost: form.mudPointCost,
workspaceStage: form.workspaceStage,
generationStage: form.generationStage,
resultStage: form.resultStage,
fields: form.fields.map((field) => ({
id: field.fieldId,
kind: field.kind,
label: field.label,
required: field.required,
})),
});
}
function validateUnifiedCreationSpecForEntry(
entryId: string,
spec: UnifiedCreationSpecPayload | null,
) {
if (!spec) {
return {ok: true as const, spec: null};
}
if (spec.playId !== entryId) {
return {ok: false as const, message: '统一创作契约 playId 必须与入口 ID 一致'};
}
return parsed;
return validateUnifiedCreationSpec(spec);
}
function normalizeMudPointCost(value: number | null | undefined) {
return typeof value === 'number' && Number.isFinite(value) && value > 0
? Math.trunc(value)
: 10;
}
function formatMudPointCostText(value: number | null | undefined) {
return `${normalizeMudPointCost(value)}泥点数`;
}
function validateUnifiedCreationSpec(value: unknown) {
@@ -635,11 +1073,18 @@ function validateUnifiedCreationSpec(value: unknown) {
}
const title = readRequiredString(value, 'title');
const mudPointCost = readPositiveInteger(value, 'mudPointCost');
const workspaceStage = readRequiredString(value, 'workspaceStage');
const generationStage = readRequiredString(value, 'generationStage');
const resultStage = readRequiredString(value, 'resultStage');
if (!title || !workspaceStage || !generationStage || !resultStage) {
return {ok: false as const, message: '统一创作契约标题和阶段不能为空'};
if (!title) {
return {ok: false as const, message: '统一创作契约标题不能为空'};
}
if (!workspaceStage || !generationStage || !resultStage) {
return {ok: false as const, message: '该玩法缺少默认阶段配置,请先由开发接入'};
}
if (mudPointCost === null) {
return {ok: false as const, message: '统一创作契约泥点消耗数量必须是大于 0 的整数'};
}
if (new Set([workspaceStage, generationStage, resultStage]).size !== 3) {
return {ok: false as const, message: '统一创作契约阶段不能重复'};
@@ -683,6 +1128,7 @@ function validateUnifiedCreationSpec(value: unknown) {
spec: {
playId,
title,
mudPointCost,
workspaceStage,
generationStage,
resultStage,
@@ -691,70 +1137,57 @@ function validateUnifiedCreationSpec(value: unknown) {
};
}
function UnifiedCreationSpecSummary({specJson}: {specJson: string}) {
const parsed = parseUnifiedCreationSpecSummaryJson(specJson);
if (!parsed.ok || !parsed.spec) {
return (
<div className="admin-alert" role="status">
{'message' in parsed ? parsed.message : '未配置统一创作页契约'}
</div>
);
}
function UnifiedCreationSpecCard({spec}: {spec: UnifiedCreationSpecPayload}) {
return (
<dl className="admin-info-list">
<div>
<dt></dt>
<dd>{parsed.spec.playId}</dd>
<div className="admin-contract-card">
<dl className="admin-info-list">
<div>
<dt></dt>
<dd>{spec.playId}</dd>
</div>
<div>
<dt></dt>
<dd>{spec.title}</dd>
</div>
<div>
<dt></dt>
<dd>{formatMudPointCostText(spec.mudPointCost)}</dd>
</div>
</dl>
<div className="admin-contract-field-list">
{spec.fields.map((field) => (
<div className="admin-contract-field-card" key={field.id}>
<strong>{field.id}</strong>
<span>{field.label}</span>
<span>
{field.kind} / {field.required ? '必填' : '选填'}
</span>
</div>
))}
</div>
<div>
<dt></dt>
<dd>{parsed.spec.title}</dd>
</div>
<div>
<dt></dt>
<dd>
{parsed.spec.workspaceStage} / {parsed.spec.generationStage} /{' '}
{parsed.spec.resultStage}
</dd>
</div>
<div>
<dt></dt>
<dd>{parsed.spec.fields.map((field) => field.id).join('、')}</dd>
</div>
</dl>
</div>
);
}
function parseUnifiedCreationSpecSummaryJson(value: string) {
const trimmed = value.trim();
if (!trimmed) {
return {ok: true as const, spec: null};
}
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch (error) {
return {
ok: false as const,
message: error instanceof Error ? `契约 JSON 非法:${error.message}` : '契约 JSON 非法',
};
}
const validation = validateUnifiedCreationSpec(parsed);
if (!validation.ok) {
return validation;
}
return {ok: true as const, spec: validation.spec};
}
function readRequiredString(value: Record<string, unknown>, key: string) {
const raw = value[key];
return typeof raw === 'string' ? raw.trim() : '';
}
function readPositiveInteger(value: Record<string, unknown>, key: string) {
const raw = value[key];
const numberValue =
typeof raw === 'number'
? raw
: typeof raw === 'string'
? Number(raw.trim())
: NaN;
if (!Number.isInteger(numberValue) || numberValue <= 0) {
return null;
}
return numberValue;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

View File

@@ -791,6 +791,67 @@ button:disabled {
overflow-wrap: anywhere;
}
.admin-contract-card {
display: grid;
gap: 12px;
border: 1px solid #eaded2;
border-radius: 8px;
background: #fffdf9;
padding: 12px;
}
.admin-contract-field-list,
.admin-contract-field-editor-list {
display: grid;
gap: 10px;
}
.admin-contract-field-list {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.admin-contract-field-card,
.admin-contract-field-editor {
display: grid;
gap: 6px;
border: 1px solid #eaded2;
border-radius: 8px;
background: #fff8f1;
padding: 10px;
}
.admin-contract-field-card strong,
.admin-contract-field-card span {
min-width: 0;
overflow-wrap: anywhere;
}
.admin-contract-field-card strong {
color: #3d1f10;
font-size: 13px;
}
.admin-contract-field-card span {
color: #8f7868;
font-size: 12px;
font-weight: 650;
}
.admin-contract-dialog {
width: min(100%, 860px);
}
.admin-contract-field-editor-grid {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(120px, 0.6fr) minmax(0, 1fr) auto;
gap: 10px;
align-items: end;
}
.admin-contract-required-toggle {
min-height: 42px;
}
.admin-status {
display: inline-flex;
max-width: 460px;
@@ -948,7 +1009,8 @@ button:disabled {
.admin-two-column-wide,
.admin-form-row,
.admin-filter-grid,
.admin-table-query-grid {
.admin-table-query-grid,
.admin-contract-field-editor-grid {
grid-template-columns: 1fr;
}