feat(admin): full legacy controller set in scene-inputs editor
Build backend images / build content-svc (push) Failing after 2m19s
Build backend images / build file-svc (push) Failing after 1m18s
Build backend images / build gateway (push) Failing after 2m38s
Build backend images / build identity-svc (push) Failing after 6m44s
Build backend images / build notification-svc (push) Failing after 1m0s
Build backend images / build render-svc (push) Failing after 58s
Build backend images / build studio-svc (push) Failing after 59s

The V2 scene-inputs editor only exposed ~15 of the content model's
~40 fields. Restore full parity with the legacy admin controller.

content-svc:
- SaveContentElementRequest + ContentElementResponse widened to the
  complete field set (text/font, direction/RTL, media, advanced, DP)
- ApplyElement / ToElementResponse map every field 1:1
  (Enum.TryParse for JustifyKind + AiInputType)

frontend (SceneInputsEditor):
- common fields up top; an "advanced" toggle reveals grouped sections:
  Text and Font, Direction (RTL/LTR), Media, Advanced, Design-Presets (DP)
- editing an element loads the full field set; rows show font/hidden badges
- nullable numbers sent as null, enums as named values (snake_case body)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 21:37:58 +03:30
parent bf6c04aba3
commit da3f92fbe8
3 changed files with 217 additions and 116 deletions
@@ -211,25 +211,60 @@ public class SceneColorService(ContentDbContext db)
{ {
e.Key = req.Key; e.Key = req.Key;
e.Title = req.Title; e.Title = req.Title;
e.LocalizedTitle = req.LocalizedTitle;
e.Hint = req.Hint; e.Hint = req.Hint;
e.Type = Enum.TryParse<ContentElementType>(req.Type, true, out var t) ? t : ContentElementType.Text; e.Type = Enum.TryParse<ContentElementType>(req.Type, true, out var t) ? t : ContentElementType.Text;
e.DefaultValue = req.DefaultValue; e.DefaultValue = req.DefaultValue;
e.PositionInContainer = req.PositionInContainer; e.PositionInContainer = req.PositionInContainer;
// text + font
e.IsTextBox = req.IsTextBox; e.IsTextBox = req.IsTextBox;
e.MaxSize = req.MaxSize; e.MaxSize = req.MaxSize;
e.FontSize = req.FontSize; e.FontSize = req.FontSize;
e.DefaultFontSize = req.DefaultFontSize;
e.FontFace = req.FontFace;
e.FontFaceName = req.FontFaceName;
e.DefaultFontFace = req.DefaultFontFace;
e.IsFontChangeable = req.IsFontChangeable; e.IsFontChangeable = req.IsFontChangeable;
e.IsFontSizeChangeable = req.IsFontSizeChangeable; e.IsFontSizeChangeable = req.IsFontSizeChangeable;
if (req.Justify != null && Enum.TryParse<JustifyKind>(req.Justify, true, out var j)) e.Justify = j;
e.CanJustify = req.CanJustify;
// direction / RTL
e.DirectionLayerKey = req.DirectionLayerKey;
e.DirectionLayerValue = req.DirectionLayerValue;
// media
e.VideoSupport = req.VideoSupport; e.VideoSupport = req.VideoSupport;
e.Width = req.Width; e.Width = req.Width;
e.Height = req.Height; e.Height = req.Height;
e.Thumbnail = req.Thumbnail; e.Thumbnail = req.Thumbnail;
e.MinDurationSec = req.MinDurationSec;
e.MaxDurationSec = req.MaxDurationSec;
// advanced
e.MappedList = req.MappedList;
e.CounterMode = req.CounterMode;
if (req.AiInputType != null && Enum.TryParse<AiInputType>(req.AiInputType, true, out var ai)) e.AiInputType = ai;
e.IsHidden = req.IsHidden;
e.IsFocused = req.IsFocused;
e.OpacityControllerKey = req.OpacityControllerKey;
e.VirtualCount = req.VirtualCount;
// design-preset variants
e.Dp1Image = req.Dp1Image; e.Dp1Title = req.Dp1Title;
e.Dp2Image = req.Dp2Image; e.Dp2Title = req.Dp2Title;
e.Dp3Image = req.Dp3Image; e.Dp3Title = req.Dp3Title;
e.Dp4Image = req.Dp4Image; e.Dp4Title = req.Dp4Title;
} }
private static ContentElementResponse ToElementResponse(SceneContentElement e) => new( private static ContentElementResponse ToElementResponse(SceneContentElement e) => new(
e.Id, e.SceneId, e.Key, e.Title, e.Hint, e.Type.ToString(), e.DefaultValue, e.Id, e.SceneId, e.Key, e.Title, e.LocalizedTitle, e.Hint, e.Type.ToString(), e.DefaultValue,
e.PositionInContainer, e.IsTextBox, e.MaxSize, e.FontSize, e.IsFontChangeable, e.PositionInContainer,
e.IsFontSizeChangeable, e.VideoSupport, e.Width, e.Height, e.Thumbnail); e.IsTextBox, e.MaxSize, e.FontSize, e.DefaultFontSize,
e.FontFace, e.FontFaceName, e.DefaultFontFace,
e.IsFontChangeable, e.IsFontSizeChangeable, e.Justify.ToString(), e.CanJustify,
e.DirectionLayerKey, e.DirectionLayerValue,
e.VideoSupport, e.Width, e.Height, e.Thumbnail, e.MinDurationSec, e.MaxDurationSec,
e.MappedList, e.CounterMode, e.AiInputType.ToString(),
e.IsHidden, e.IsFocused, e.OpacityControllerKey, e.VirtualCount,
e.Dp1Image, e.Dp1Title, e.Dp2Image, e.Dp2Title,
e.Dp3Image, e.Dp3Title, e.Dp4Image, e.Dp4Title);
// ── helpers ───────────────────────────────────────────────────────────── // ── helpers ─────────────────────────────────────────────────────────────
@@ -52,17 +52,39 @@ public record SaveColorPresetRequest(
// ── Scene content elements (the editable inputs inside a scene) ─────────────── // ── Scene content elements (the editable inputs inside a scene) ───────────────
public record ContentElementResponse( public record ContentElementResponse(
Guid Id, Guid SceneId, string Key, string Title, string? Hint, Guid Id, Guid SceneId, string Key, string Title, string? LocalizedTitle, string? Hint,
string Type, string? DefaultValue, int PositionInContainer, string Type, string? DefaultValue, int PositionInContainer,
bool IsTextBox, int? MaxSize, int? FontSize, bool IsFontChangeable, // text + font
bool IsFontSizeChangeable, bool VideoSupport, int? Width, int? Height, bool IsTextBox, int? MaxSize, int? FontSize, int? DefaultFontSize,
string? Thumbnail string? FontFace, string? FontFaceName, string? DefaultFontFace,
bool IsFontChangeable, bool IsFontSizeChangeable, string Justify, bool CanJustify,
// direction / RTL
string? DirectionLayerKey, int DirectionLayerValue,
// media
bool VideoSupport, int? Width, int? Height, string? Thumbnail,
decimal? MinDurationSec, decimal? MaxDurationSec,
// advanced
string? MappedList, string? CounterMode, string AiInputType,
bool IsHidden, bool IsFocused, string? OpacityControllerKey, int VirtualCount,
// design-preset (dp) variants
string? Dp1Image, string? Dp1Title, string? Dp2Image, string? Dp2Title,
string? Dp3Image, string? Dp3Title, string? Dp4Image, string? Dp4Title
); );
public record SaveContentElementRequest( public record SaveContentElementRequest(
Guid SceneId, string Key, string Title, string? Hint, Guid SceneId, string Key, string Title, string Type,
string Type, string? DefaultValue, int PositionInContainer, string? LocalizedTitle = null, string? Hint = null, string? DefaultValue = null,
bool IsTextBox, int? MaxSize, int? FontSize, int PositionInContainer = 0,
bool IsFontChangeable, bool IsFontSizeChangeable, bool IsTextBox = false, int? MaxSize = null, int? FontSize = null, int? DefaultFontSize = null,
bool VideoSupport, int? Width, int? Height, string? Thumbnail string? FontFace = null, string? FontFaceName = null, string? DefaultFontFace = null,
bool IsFontChangeable = false, bool IsFontSizeChangeable = false,
string? Justify = null, bool CanJustify = true,
string? DirectionLayerKey = null, int DirectionLayerValue = 0,
bool VideoSupport = false, int? Width = null, int? Height = null, string? Thumbnail = null,
decimal? MinDurationSec = null, decimal? MaxDurationSec = null,
string? MappedList = null, string? CounterMode = null, string? AiInputType = null,
bool IsHidden = false, bool IsFocused = false, string? OpacityControllerKey = null,
int VirtualCount = 1,
string? Dp1Image = null, string? Dp1Title = null, string? Dp2Image = null, string? Dp2Title = null,
string? Dp3Image = null, string? Dp3Title = null, string? Dp4Image = null, string? Dp4Title = null
); );
+148 -104
View File
@@ -722,20 +722,39 @@ function PresetsTab({
// ── Scene inputs (content elements) ─────────────────────────────────────────── // ── Scene inputs (content elements) ───────────────────────────────────────────
interface ContentElement { interface ContentElement {
id: string; id: string; scene_id: string; key: string; title: string; localized_title?: string | null;
scene_id: string; hint?: string | null; type: string; default_value?: string | null; position_in_container: number;
key: string; is_text_box: boolean; max_size?: number | null; font_size?: number | null; default_font_size?: number | null;
title: string; font_face?: string | null; font_face_name?: string | null; default_font_face?: string | null;
hint?: string | null; is_font_changeable: boolean; is_font_size_changeable: boolean; justify: string; can_justify: boolean;
type: string; direction_layer_key?: string | null; direction_layer_value: number;
default_value?: string | null; video_support: boolean; width?: number | null; height?: number | null; thumbnail?: string | null;
position_in_container: number; min_duration_sec?: number | null; max_duration_sec?: number | null;
mapped_list?: string | null; counter_mode?: string | null; ai_input_type: string;
is_hidden: boolean; is_focused: boolean; opacity_controller_key?: string | null; virtual_count: number;
dp1_image?: string | null; dp1_title?: string | null; dp2_image?: string | null; dp2_title?: string | null;
dp3_image?: string | null; dp3_title?: string | null; dp4_image?: string | null; dp4_title?: string | null;
} }
const ELEMENT_TYPES = [ const ELEMENT_TYPES = [
"Text", "TextArea", "Media", "Audio", "Voiceover", "CheckBox", "Text", "TextArea", "Media", "Audio", "Voiceover", "CheckBox",
"DropDown", "Fill", "Color", "Number", "Date", "Toggle", "Slider", "Counter", "Hidden", "DropDown", "Fill", "Color", "Number", "Date", "Toggle", "Slider", "Counter", "Hidden",
]; ];
const JUSTIFY_KINDS = ["LEFT_JUSTIFY", "CENTER_JUSTIFY", "RIGHT_JUSTIFY", "FULL_JUSTIFY"];
const AI_INPUT_TYPES = ["None", "TitleSuggest", "BodySuggest", "TranslateRtl", "TranslateLtr", "RemoveBG", "UpscaleImage", "TTS"];
type ElDraft = Record<string, string | number | boolean | null>;
const emptyEl: ElDraft = {
key: "", title: "", localized_title: "", type: "Text", default_value: "", hint: "", position_in_container: 0,
is_text_box: false, max_size: null, font_size: null, default_font_size: null,
font_face: "", font_face_name: "", default_font_face: "", is_font_changeable: false, is_font_size_changeable: false,
justify: "CENTER_JUSTIFY", can_justify: true,
direction_layer_key: "", direction_layer_value: 0,
video_support: false, width: null, height: null, thumbnail: "", min_duration_sec: null, max_duration_sec: null,
mapped_list: "", counter_mode: "", ai_input_type: "None", is_hidden: false, is_focused: false,
opacity_controller_key: "", virtual_count: 1,
dp1_image: "", dp1_title: "", dp2_image: "", dp2_title: "", dp3_image: "", dp3_title: "", dp4_image: "", dp4_title: "",
};
export function SceneInputsEditor({ export function SceneInputsEditor({
sceneId, sceneId,
@@ -746,17 +765,15 @@ export function SceneInputsEditor({
}) { }) {
const [items, setItems] = useState<ContentElement[]>([]); const [items, setItems] = useState<ContentElement[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const empty = { key: "", title: "", type: "Text", default_value: "", hint: "", position_in_container: 0 }; const [draft, setDraft] = useState<ElDraft>({ ...emptyEl });
const [draft, setDraft] = useState<typeof empty>({ ...empty });
const [editId, setEditId] = useState<string | null>(null); const [editId, setEditId] = useState<string | null>(null);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [advanced, setAdvanced] = useState(false);
const reload = useCallback(async () => { const reload = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const r = await fetch(`/api/admin/resource/scene-elements?scene_id=${sceneId}`, { const r = await fetch(`/api/admin/resource/scene-elements?scene_id=${sceneId}`, { cache: "no-store" }).then((x) => x.json());
cache: "no-store",
}).then((x) => x.json());
setItems(Array.isArray(r) ? r : (r?.items ?? [])); setItems(Array.isArray(r) ? r : (r?.items ?? []));
} catch { } catch {
setError("بارگذاری ورودی‌ها ناموفق بود"); setError("بارگذاری ورودی‌ها ناموفق بود");
@@ -766,30 +783,22 @@ export function SceneInputsEditor({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [sceneId]); }, [sceneId]);
useEffect(() => { useEffect(() => { reload(); }, [reload]);
reload();
}, [reload]); const set = (patch: ElDraft) => setDraft((d) => ({ ...d, ...patch }));
const submit = async () => { const submit = async () => {
setBusy(true); setBusy(true);
setError(null); setError(null);
const body = { scene_id: sceneId, is_text_box: draft.type === "TextArea", ...draft }; const url = editId ? `/api/admin/resource/scene-elements/${editId}` : `/api/admin/resource/scene-elements`;
const url = editId
? `/api/admin/resource/scene-elements/${editId}`
: `/api/admin/resource/scene-elements`;
const res = await fetch(url, { const res = await fetch(url, {
method: editId ? "PUT" : "POST", method: editId ? "PUT" : "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body), body: JSON.stringify({ ...draft, scene_id: sceneId }),
}); });
setBusy(false); setBusy(false);
if (res.ok) { if (res.ok) { setDraft({ ...emptyEl }); setEditId(null); reload(); }
setDraft({ ...empty }); else setError("ذخیرهٔ ورودی ناموفق بود");
setEditId(null);
reload();
} else {
setError("ذخیرهٔ ورودی ناموفق بود");
}
}; };
const remove = async (el: ContentElement) => { const remove = async (el: ContentElement) => {
@@ -798,105 +807,140 @@ export function SceneInputsEditor({
if (res.ok) reload(); if (res.ok) reload();
}; };
const startEdit = (el: ContentElement) => {
setEditId(el.id);
const next: ElDraft = { ...emptyEl };
for (const k of Object.keys(emptyEl)) {
const v = (el as unknown as Record<string, unknown>)[k];
if (v !== undefined && v !== null) next[k] = v as string | number | boolean;
}
setDraft(next);
setAdvanced(true);
};
// ── field render helpers (typed, no `any`) ──────────────────────────────────
const T = (label: string, k: string, span?: number, ltr?: boolean) => (
<div className={span === 2 ? "sm:col-span-2" : span === 3 ? "sm:col-span-3" : ""}>
<label className={lbl}>{label}</label>
<input className={inp} dir={ltr ? "ltr" : undefined} value={String(draft[k] ?? "")}
onChange={(e) => set({ [k]: e.target.value })} />
</div>
);
const N = (label: string, k: string) => (
<div>
<label className={lbl}>{label}</label>
<input className={inp} type="number"
value={draft[k] === null || draft[k] === undefined ? "" : String(draft[k])}
onChange={(e) => set({ [k]: e.target.value === "" ? null : Number(e.target.value) })} />
</div>
);
const Bx = (label: string, k: string) => (
<label className="flex items-center gap-2 py-1 text-[11px] text-gray-300">
<input type="checkbox" checked={Boolean(draft[k])} onChange={(e) => set({ [k]: e.target.checked })} />
{label}
</label>
);
const Sel = (label: string, k: string, opts: string[]) => (
<div>
<label className={lbl}>{label}</label>
<select className={inp} value={String(draft[k] ?? "")} onChange={(e) => set({ [k]: e.target.value })}>
{opts.map((o) => <option key={o} value={o}>{o}</option>)}
</select>
</div>
);
const Group = (title: string) => (
<p className="sm:col-span-3 mt-1 border-t border-[#1e2235] pt-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">{title}</p>
);
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<p className="text-[11px] font-medium text-gray-400"> <p className="text-[11px] font-medium text-gray-400">ورودیهای قابل ویرایش این صحنه (متن، تصویر، رنگ )</p>
ورودیهای قابل ویرایش این صحنه (متن، تصویر، رنگ )
</p>
{loading ? ( {loading ? (
<p className="text-[11px] text-gray-600">در حال بارگذاری</p> <p className="text-[11px] text-gray-600">در حال بارگذاری</p>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{items.length === 0 && ( {items.length === 0 && <p className="text-[11px] text-gray-600">هنوز ورودیای تعریف نشده است.</p>}
<p className="text-[11px] text-gray-600">هنوز ورودیای تعریف نشده است.</p>
)}
{items.map((el) => ( {items.map((el) => (
<div <div key={el.id} className="flex flex-wrap items-center gap-2 rounded border border-[#1e2235] bg-[#070811] px-2 py-1.5">
key={el.id} <span className="rounded bg-violet-500/15 px-1.5 py-0.5 text-[10px] text-violet-300">{el.type}</span>
className="flex flex-wrap items-center gap-2 rounded border border-[#1e2235] bg-[#070811] px-2 py-1.5"
>
<span className="rounded bg-violet-500/15 px-1.5 py-0.5 text-[10px] text-violet-300">
{el.type}
</span>
<span className="flex-1 truncate text-[11px] text-gray-200"> <span className="flex-1 truncate text-[11px] text-gray-200">
{el.title}{" "} {el.title} <span className="text-gray-600" dir="ltr">({el.key})</span>
<span className="text-gray-600" dir="ltr">({el.key})</span>
</span> </span>
{el.default_value && ( {el.default_value && <span className="max-w-[120px] truncate text-[10px] text-gray-500" dir="auto">{el.default_value}</span>}
<span className="max-w-[120px] truncate text-[10px] text-gray-500" dir="auto"> {el.is_font_changeable && <span className="rounded bg-blue-500/15 px-1 text-[9px] text-blue-300">فونت</span>}
{el.default_value} {el.is_hidden && <span className="rounded bg-gray-500/15 px-1 text-[9px] text-gray-400">مخفی</span>}
</span>
)}
<span className="text-[10px] text-gray-600">#{el.position_in_container}</span> <span className="text-[10px] text-gray-600">#{el.position_in_container}</span>
<button <button className={ghost} onClick={() => startEdit(el)}>ویرایش</button>
className={ghost} <button className={del} onClick={() => remove(el)}>حذف</button>
onClick={() => {
setEditId(el.id);
setDraft({
key: el.key,
title: el.title,
type: el.type,
default_value: el.default_value ?? "",
hint: el.hint ?? "",
position_in_container: el.position_in_container,
});
}}
>
ویرایش
</button>
<button className={del} onClick={() => remove(el)}>
حذف
</button>
</div> </div>
))} ))}
</div> </div>
)} )}
{/* Add / edit input */} {/* Add / edit input — full controller set */}
<div className="rounded border border-dashed border-[#262b40] p-2"> <div className="rounded border border-dashed border-[#262b40] p-2">
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3"> <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
<div> {T("کلید (یکتا)", "key", undefined, true)}
<label className={lbl}>کلید (یکتا)</label> {T("عنوان", "title")}
<input className={inp} dir="ltr" value={draft.key} {Sel("نوع", "type", ELEMENT_TYPES)}
onChange={(e) => setDraft({ ...draft, key: e.target.value })} /> {T("مقدار پیش‌فرض", "default_value", 2)}
</div> {N("ترتیب", "position_in_container")}
<div> {T("عنوان محلی (FA/EN JSON)", "localized_title", 2)}
<label className={lbl}>عنوان</label> {T("راهنما", "hint")}
<input className={inp} value={draft.title}
onChange={(e) => setDraft({ ...draft, title: e.target.value })} /> {advanced && (<>
</div> {Group("متن و فونت")}
<div> {Bx("جعبهٔ متن (TextBox)", "is_text_box")}
<label className={lbl}>نوع</label> {Bx("فونت قابل تغییر", "is_font_changeable")}
<select className={inp} value={draft.type} {Bx("اندازهٔ فونت قابل تغییر", "is_font_size_changeable")}
onChange={(e) => setDraft({ ...draft, type: e.target.value })}> {N("حداکثر طول متن", "max_size")}
{ELEMENT_TYPES.map((t) => <option key={t} value={t}>{t}</option>)} {N("اندازهٔ فونت", "font_size")}
</select> {N("اندازهٔ فونت پیش‌فرض", "default_font_size")}
</div> {T("فونت (face)", "font_face", undefined, true)}
<div className="sm:col-span-2"> {T("نام فونت", "font_face_name")}
<label className={lbl}>مقدار پیشفرض</label> {T("فونت پیش‌فرض", "default_font_face", undefined, true)}
<input className={inp} value={draft.default_value} {Sel("چینش متن", "justify", JUSTIFY_KINDS)}
onChange={(e) => setDraft({ ...draft, default_value: e.target.value })} /> {Bx("چینش قابل تغییر", "can_justify")}
</div>
<div> {Group("جهت (RTL/LTR)")}
<label className={lbl}>ترتیب</label> {T("کلید لایهٔ جهت", "direction_layer_key", undefined, true)}
<input className={inp} type="number" value={draft.position_in_container} {N("مقدار جهت", "direction_layer_value")}
onChange={(e) => setDraft({ ...draft, position_in_container: Number(e.target.value) })} />
</div> {Group("رسانه")}
<div className="sm:col-span-3"> {Bx("پشتیبانی ویدیو", "video_support")}
<label className={lbl}>راهنما (اختیاری)</label> {N("عرض", "width")}
<input className={inp} value={draft.hint} {N("ارتفاع", "height")}
onChange={(e) => setDraft({ ...draft, hint: e.target.value })} /> {N("حداقل مدت (ثانیه)", "min_duration_sec")}
</div> {N("حداکثر مدت (ثانیه)", "max_duration_sec")}
{T("تصویر بندانگشتی", "thumbnail", 3)}
{Group("پیشرفته")}
{Sel("نوع ورودی هوش مصنوعی", "ai_input_type", AI_INPUT_TYPES)}
{T("حالت شمارنده", "counter_mode")}
{N("تعداد مجازی (Virtual)", "virtual_count")}
{Bx("مخفی", "is_hidden")}
{Bx("فوکوس", "is_focused")}
{T("کلید کنترل شفافیت", "opacity_controller_key", undefined, true)}
{T("لیست نگاشت (JSON)", "mapped_list", 3)}
{Group("حالت‌های طراحی (DP)")}
{T("DP1 تصویر", "dp1_image")} {T("DP1 عنوان", "dp1_title")}
{T("DP2 تصویر", "dp2_image")} {T("DP2 عنوان", "dp2_title")}
{T("DP3 تصویر", "dp3_image")} {T("DP3 عنوان", "dp3_title")}
{T("DP4 تصویر", "dp4_image")} {T("DP4 عنوان", "dp4_title")}
</>)}
</div> </div>
<div className="mt-2 flex gap-2">
<div className="mt-2 flex items-center gap-2">
<button className={btn} onClick={submit} disabled={busy || !draft.key || !draft.title}> <button className={btn} onClick={submit} disabled={busy || !draft.key || !draft.title}>
{busy ? "…" : editId ? "ذخیرهٔ تغییرات" : "+ افزودن ورودی"} {busy ? "…" : editId ? "ذخیرهٔ تغییرات" : "+ افزودن ورودی"}
</button> </button>
{editId && ( {editId && (
<button className={ghost} onClick={() => { setEditId(null); setDraft({ ...empty }); }}> <button className={ghost} onClick={() => { setEditId(null); setDraft({ ...emptyEl }); }}>انصراف</button>
انصراف
</button>
)} )}
<button type="button" className="ms-auto text-[11px] text-indigo-300 hover:underline" onClick={() => setAdvanced((v) => !v)}>
{advanced ? "پنهان کردن تنظیمات پیشرفته ▲" : "تنظیمات پیشرفته (فونت، جهت، رسانه، DP …) ▼"}
</button>
</div> </div>
</div> </div>
</div> </div>