702 lines
22 KiB
TypeScript
702 lines
22 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect } from "react";
|
||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||
|
||
import { InfoScreenConfig } from "./InfoScreenConfig";
|
||
import { DateScreenConfig } from "./DateScreenConfig";
|
||
import { CouponScreenConfig } from "./CouponScreenConfig";
|
||
import { FormScreenConfig } from "./FormScreenConfig";
|
||
import { ListScreenConfig } from "./ListScreenConfig";
|
||
import { EmailScreenConfig } from "./EmailScreenConfig";
|
||
import { LoadersScreenConfig } from "./LoadersScreenConfig";
|
||
import { SoulmatePortraitScreenConfig } from "./SoulmatePortraitScreenConfig";
|
||
import { TrialPaymentScreenConfig } from "./TrialPaymentScreenConfig";
|
||
import { TrialChoiceScreenConfig } from "./TrialChoiceScreenConfig";
|
||
|
||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||
import { TextAreaInput } from "@/components/ui/TextAreaInput/TextAreaInput";
|
||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||
import type {
|
||
ScreenDefinition,
|
||
InfoScreenDefinition,
|
||
DateScreenDefinition,
|
||
CouponScreenDefinition,
|
||
FormScreenDefinition,
|
||
ListScreenDefinition,
|
||
EmailScreenDefinition,
|
||
LoadersScreenDefinition,
|
||
SoulmatePortraitScreenDefinition,
|
||
TypographyVariant,
|
||
BottomActionButtonDefinition,
|
||
HeaderDefinition,
|
||
TrialPaymentScreenDefinition,
|
||
SpecialOfferScreenDefinition,
|
||
TrialChoiceScreenDefinition,
|
||
} from "@/lib/funnel/types";
|
||
import { SpecialOfferScreenConfig } from "./SpecialOfferScreenConfig";
|
||
|
||
const RADIUS_OPTIONS: ("3xl" | "full")[] = ["3xl", "full"];
|
||
|
||
interface TemplateConfigProps {
|
||
screen: BuilderScreen;
|
||
onUpdate: (updates: Partial<ScreenDefinition>) => void;
|
||
}
|
||
|
||
function CollapsibleSection({
|
||
title,
|
||
children,
|
||
defaultExpanded = false,
|
||
}: {
|
||
title: string;
|
||
children: React.ReactNode;
|
||
defaultExpanded?: boolean;
|
||
}) {
|
||
const storageKey = `template-section-${title
|
||
.toLowerCase()
|
||
.replace(/\s+/g, "-")}`;
|
||
|
||
const [isExpanded, setIsExpanded] = useState(() => {
|
||
if (typeof window === "undefined") return defaultExpanded;
|
||
|
||
const stored = sessionStorage.getItem(storageKey);
|
||
return stored !== null ? JSON.parse(stored) : defaultExpanded;
|
||
});
|
||
|
||
const handleToggle = () => {
|
||
const newExpanded = !isExpanded;
|
||
setIsExpanded(newExpanded);
|
||
|
||
if (typeof window !== "undefined") {
|
||
sessionStorage.setItem(storageKey, JSON.stringify(newExpanded));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<button
|
||
type="button"
|
||
onClick={handleToggle}
|
||
className="flex w-full items-center gap-2 text-left text-sm font-medium text-foreground hover:text-primary transition-colors"
|
||
>
|
||
{isExpanded ? (
|
||
<ChevronDown className="h-4 w-4" />
|
||
) : (
|
||
<ChevronRight className="h-4 w-4" />
|
||
)}
|
||
{title}
|
||
</button>
|
||
{isExpanded && <div className="ml-6 space-y-3">{children}</div>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
interface TypographyControlsProps {
|
||
label: string;
|
||
value: (TypographyVariant & { show?: boolean }) | undefined;
|
||
onChange: (
|
||
value: (TypographyVariant & { show?: boolean }) | undefined
|
||
) => void;
|
||
allowRemove?: boolean;
|
||
showToggle?: boolean; // Показывать ли чекбокс "Показывать"
|
||
}
|
||
|
||
function TypographyControls({
|
||
label,
|
||
value,
|
||
onChange,
|
||
allowRemove = false,
|
||
showToggle = false,
|
||
}: TypographyControlsProps) {
|
||
const storageKey = `typography-advanced-${label
|
||
.toLowerCase()
|
||
.replace(/\s+/g, "-")}`;
|
||
|
||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||
const [isHydrated, setIsHydrated] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const stored = sessionStorage.getItem(storageKey);
|
||
if (stored !== null) {
|
||
setShowAdvanced(JSON.parse(stored));
|
||
}
|
||
setIsHydrated(true);
|
||
}, [storageKey]);
|
||
|
||
const handleTextChange = (text: string) => {
|
||
// Всегда обновляем текст, даже если пустой
|
||
// Это позволяет controlled input работать корректно
|
||
onChange({
|
||
...value,
|
||
text,
|
||
show: value?.show ?? true, // Если show не задан, по умолчанию true
|
||
});
|
||
};
|
||
|
||
const handleTextBlur = () => {
|
||
// При потере фокуса удаляем объект если текст пустой
|
||
if (allowRemove && (!value?.text || value.text.trim() === "")) {
|
||
onChange(undefined);
|
||
}
|
||
};
|
||
|
||
const handleAdvancedChange = (
|
||
field: keyof TypographyVariant,
|
||
fieldValue: string
|
||
) => {
|
||
onChange({
|
||
...value,
|
||
text: value?.text || "",
|
||
[field]: fieldValue || undefined,
|
||
});
|
||
};
|
||
|
||
const handleShowToggle = (show: boolean) => {
|
||
if (!show) {
|
||
// Скрываем элемент
|
||
if (allowRemove) {
|
||
// Для опциональных полей - удаляем объект полностью
|
||
onChange(undefined);
|
||
} else {
|
||
// Для обязательных полей - сохраняем с show: false
|
||
onChange({
|
||
...value,
|
||
text: value?.text || "",
|
||
show: false,
|
||
});
|
||
}
|
||
} else {
|
||
// Показываем элемент
|
||
onChange({
|
||
...value,
|
||
text: value?.text || "",
|
||
show: true,
|
||
});
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
{showToggle && (
|
||
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
||
<input
|
||
type="checkbox"
|
||
checked={value ? value.show ?? true : false}
|
||
onChange={(event) => handleShowToggle(event.target.checked)}
|
||
/>
|
||
Показывать {label.toLowerCase()}
|
||
</label>
|
||
)}
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium text-foreground">{label}</label>
|
||
<TextAreaInput
|
||
value={value?.text ?? ""}
|
||
onChange={(event) => handleTextChange(event.target.value)}
|
||
onBlur={handleTextBlur}
|
||
rows={2}
|
||
className="resize-y"
|
||
aria-invalid={
|
||
!allowRemove && (!value?.text || value.text.trim() === "")
|
||
}
|
||
/>
|
||
{!allowRemove && (!value?.text || value.text.trim() === "") && (
|
||
<p className="text-xs text-destructive">
|
||
Это поле обязательно для заполнения
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{value?.text && (
|
||
<div className="space-y-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const newShowAdvanced = !showAdvanced;
|
||
setShowAdvanced(newShowAdvanced);
|
||
if (typeof window !== "undefined") {
|
||
sessionStorage.setItem(
|
||
storageKey,
|
||
JSON.stringify(newShowAdvanced)
|
||
);
|
||
}
|
||
}}
|
||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition"
|
||
>
|
||
{showAdvanced ? (
|
||
<ChevronDown className="h-3 w-3" />
|
||
) : (
|
||
<ChevronRight className="h-3 w-3" />
|
||
)}
|
||
Настройки оформления
|
||
</button>
|
||
|
||
{(isHydrated ? showAdvanced : false) && (
|
||
<div className="ml-4 grid grid-cols-2 gap-2 text-xs">
|
||
<label className="flex flex-col gap-1">
|
||
<span className="text-muted-foreground">Шрифт</span>
|
||
<select
|
||
className="rounded border border-border bg-background px-2 py-1"
|
||
value={value?.font ?? ""}
|
||
onChange={(e) => handleAdvancedChange("font", e.target.value)}
|
||
>
|
||
<option value="">По умолчанию</option>
|
||
<option value="manrope">Manrope</option>
|
||
<option value="inter">Inter</option>
|
||
<option value="geistSans">Geist Sans</option>
|
||
<option value="geistMono">Geist Mono</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label className="flex flex-col gap-1">
|
||
<span className="text-muted-foreground">Толщина</span>
|
||
<select
|
||
className="rounded border border-border bg-background px-2 py-1"
|
||
value={value?.weight ?? ""}
|
||
onChange={(e) =>
|
||
handleAdvancedChange("weight", e.target.value)
|
||
}
|
||
>
|
||
<option value="">По умолчанию</option>
|
||
<option value="regular">Regular</option>
|
||
<option value="medium">Medium</option>
|
||
<option value="semiBold">Semi Bold</option>
|
||
<option value="bold">Bold</option>
|
||
<option value="extraBold">Extra Bold</option>
|
||
<option value="black">Black</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label className="flex flex-col gap-1">
|
||
<span className="text-muted-foreground">Выравнивание</span>
|
||
<select
|
||
className="rounded border border-border bg-background px-2 py-1"
|
||
value={value?.align ?? ""}
|
||
onChange={(e) =>
|
||
handleAdvancedChange("align", e.target.value)
|
||
}
|
||
>
|
||
<option value="">По умолчанию</option>
|
||
<option value="left">Слева</option>
|
||
<option value="center">По центру</option>
|
||
<option value="right">Справа</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
interface HeaderControlsProps {
|
||
header: HeaderDefinition | undefined;
|
||
onChange: (value: HeaderDefinition | undefined) => void;
|
||
}
|
||
|
||
function HeaderControls({ header, onChange }: HeaderControlsProps) {
|
||
const activeHeader = header ?? {
|
||
show: true,
|
||
showBackButton: true,
|
||
showProgress: true
|
||
};
|
||
|
||
const handleToggle = (
|
||
field: "show" | "showBackButton" | "showProgress",
|
||
checked: boolean
|
||
) => {
|
||
onChange({
|
||
...activeHeader,
|
||
[field]: checked,
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={activeHeader.show !== false}
|
||
onChange={(event) => handleToggle("show", event.target.checked)}
|
||
/>
|
||
Показывать шапку экрана
|
||
</label>
|
||
|
||
{activeHeader.show !== false && (
|
||
<div className="space-y-3 rounded-lg border border-border/60 bg-muted/20 p-3">
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={activeHeader.showProgress !== false}
|
||
onChange={(event) =>
|
||
handleToggle("showProgress", event.target.checked)
|
||
}
|
||
/>
|
||
Показывать прогресс бар
|
||
</label>
|
||
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={activeHeader.showBackButton !== false}
|
||
onChange={(event) =>
|
||
handleToggle("showBackButton", event.target.checked)
|
||
}
|
||
/>
|
||
Показывать кнопку «Назад»
|
||
</label>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
interface ActionButtonControlsProps {
|
||
label: string;
|
||
value: BottomActionButtonDefinition | undefined;
|
||
onChange: (value: BottomActionButtonDefinition | undefined) => void;
|
||
}
|
||
|
||
function ActionButtonControls({
|
||
label,
|
||
value,
|
||
onChange,
|
||
}: ActionButtonControlsProps) {
|
||
// По умолчанию кнопка включена (show !== false)
|
||
const isEnabled = value?.show !== false;
|
||
const buttonText = value?.text || "";
|
||
const cornerRadius = value?.cornerRadius;
|
||
const showPrivacyTermsConsent = value?.showPrivacyTermsConsent ?? false;
|
||
const showGradientBlur = value?.showGradientBlur ?? true;
|
||
|
||
const handleToggle = (enabled: boolean) => {
|
||
if (enabled) {
|
||
// Включаем кнопку - убираем show: false или создаем объект
|
||
const newValue = value ? { ...value, show: true } : { show: true };
|
||
// Если show: true по умолчанию, можем убрать это поле
|
||
if (newValue.show === true && !newValue.text && !newValue.cornerRadius) {
|
||
onChange(undefined); // Дефолтное состояние
|
||
} else {
|
||
const { show, ...rest } = newValue;
|
||
onChange(Object.keys(rest).length > 0 ? { show, ...rest } : undefined);
|
||
}
|
||
} else {
|
||
// Отключаем кнопку
|
||
onChange({ show: false });
|
||
}
|
||
};
|
||
|
||
const handleTextChange = (text: string) => {
|
||
if (!isEnabled) return;
|
||
|
||
const trimmedText = text.trim();
|
||
const newValue = {
|
||
...value,
|
||
text: trimmedText || undefined,
|
||
};
|
||
|
||
// Убираем undefined поля для чистоты
|
||
if (!newValue.text && !newValue.cornerRadius && newValue.show !== false) {
|
||
onChange(undefined);
|
||
} else {
|
||
onChange(newValue);
|
||
}
|
||
};
|
||
|
||
const handleRadiusChange = (radius: string) => {
|
||
if (!isEnabled) return;
|
||
|
||
const newRadius = (radius as "3xl" | "full") || undefined;
|
||
const newValue = {
|
||
...value,
|
||
cornerRadius: newRadius,
|
||
};
|
||
|
||
// Убираем undefined поля для чистоты
|
||
if (
|
||
!newValue.text &&
|
||
!newValue.cornerRadius &&
|
||
newValue.show !== false &&
|
||
!newValue.showPrivacyTermsConsent
|
||
) {
|
||
onChange(undefined);
|
||
} else {
|
||
onChange(newValue);
|
||
}
|
||
};
|
||
|
||
const handlePrivacyTermsToggle = (checked: boolean) => {
|
||
if (!isEnabled) return;
|
||
|
||
const newValue = {
|
||
...value,
|
||
showPrivacyTermsConsent: checked || undefined,
|
||
};
|
||
|
||
// Убираем undefined поля для чистоты
|
||
if (
|
||
!newValue.text &&
|
||
!newValue.cornerRadius &&
|
||
newValue.show !== false &&
|
||
!newValue.showPrivacyTermsConsent
|
||
) {
|
||
onChange(undefined);
|
||
} else {
|
||
onChange(newValue);
|
||
}
|
||
};
|
||
|
||
const handleGradientBlurToggle = (checked: boolean) => {
|
||
// Работает даже когда кнопка отключена (для LIST экранов)
|
||
const newValue = {
|
||
...value,
|
||
// Если checked = true (дефолт), не сохраняем поле
|
||
// Если checked = false, сохраняем явно
|
||
showGradientBlur: checked ? undefined : false,
|
||
};
|
||
|
||
// Убираем undefined поля для чистоты
|
||
if (
|
||
!newValue.text &&
|
||
!newValue.cornerRadius &&
|
||
newValue.show !== false &&
|
||
!newValue.showPrivacyTermsConsent &&
|
||
newValue.showGradientBlur !== false
|
||
) {
|
||
onChange(undefined);
|
||
} else {
|
||
onChange(newValue);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={isEnabled}
|
||
onChange={(event) => handleToggle(event.target.checked)}
|
||
/>
|
||
{label}
|
||
</label>
|
||
|
||
{isEnabled && (
|
||
<div className="space-y-3 rounded-lg border border-border/60 bg-muted/20 p-3 text-xs">
|
||
<label className="flex flex-col gap-1 text-sm">
|
||
<span className="font-medium text-muted-foreground">
|
||
Текст кнопки
|
||
</span>
|
||
<TextInput
|
||
value={buttonText}
|
||
onChange={(event) => handleTextChange(event.target.value)}
|
||
placeholder="Оставьте пустым для дефолтного текста"
|
||
/>
|
||
</label>
|
||
|
||
<label className="flex flex-col gap-1">
|
||
<span className="font-medium text-muted-foreground">
|
||
Скругление
|
||
</span>
|
||
<select
|
||
className="rounded-lg border border-border bg-background px-2 py-1"
|
||
value={cornerRadius ?? ""}
|
||
onChange={(event) => handleRadiusChange(event.target.value)}
|
||
>
|
||
<option value="">По умолчанию</option>
|
||
{RADIUS_OPTIONS.map((option) => (
|
||
<option key={option} value={option}>
|
||
{option}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={showPrivacyTermsConsent}
|
||
onChange={(event) =>
|
||
handlePrivacyTermsToggle(event.target.checked)
|
||
}
|
||
/>
|
||
Показывать PrivacyTermsConsent под кнопкой
|
||
</label>
|
||
</div>
|
||
)}
|
||
|
||
{/* Gradient Blur - доступен даже когда кнопка отключена */}
|
||
<div className="rounded-lg border border-border/60 bg-muted/20 p-3">
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={showGradientBlur}
|
||
onChange={(event) =>
|
||
handleGradientBlurToggle(event.target.checked)
|
||
}
|
||
/>
|
||
<span>
|
||
Показывать gradient blur{" "}
|
||
{!isEnabled && (
|
||
<span className="text-muted-foreground">(работает даже без кнопки)</span>
|
||
)}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function TemplateConfig({ screen, onUpdate }: TemplateConfigProps) {
|
||
const { template } = screen;
|
||
|
||
const handleTitleChange = (value: TypographyVariant | undefined) => {
|
||
// Заголовок обязательный, но разрешаем временно пустой текст
|
||
// (для корректной работы controlled input)
|
||
// Валидация при сохранении покажет ошибку если текст пустой
|
||
if (!value) {
|
||
// Создаем минимальный объект вместо undefined
|
||
onUpdate({ title: { text: "" } });
|
||
return;
|
||
}
|
||
onUpdate({ title: value });
|
||
};
|
||
|
||
const handleSubtitleChange = (newValue: TypographyVariant | undefined) => {
|
||
onUpdate({ subtitle: newValue });
|
||
};
|
||
|
||
const handleHeaderChange = (value: HeaderDefinition | undefined) => {
|
||
onUpdate({ header: value });
|
||
};
|
||
|
||
const handleButtonChange = (
|
||
value: BottomActionButtonDefinition | undefined
|
||
) => {
|
||
onUpdate({ bottomActionButton: value });
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<CollapsibleSection title="Заголовок и подзаголовок">
|
||
<TypographyControls
|
||
label="Заголовок"
|
||
value={screen.title}
|
||
onChange={handleTitleChange}
|
||
showToggle
|
||
/>
|
||
<TypographyControls
|
||
label="Подзаголовок"
|
||
value={"subtitle" in screen ? screen.subtitle : undefined}
|
||
onChange={handleSubtitleChange}
|
||
allowRemove
|
||
showToggle
|
||
/>
|
||
</CollapsibleSection>
|
||
|
||
<CollapsibleSection title="Шапка экрана">
|
||
<HeaderControls header={screen.header} onChange={handleHeaderChange} />
|
||
</CollapsibleSection>
|
||
|
||
<CollapsibleSection title="Нижняя кнопка">
|
||
<ActionButtonControls
|
||
label="Показывать основную кнопку"
|
||
value={screen.bottomActionButton}
|
||
onChange={handleButtonChange}
|
||
/>
|
||
</CollapsibleSection>
|
||
|
||
{template === "info" && (
|
||
<InfoScreenConfig
|
||
screen={screen as BuilderScreen & { template: "info" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<InfoScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "date" && (
|
||
<DateScreenConfig
|
||
screen={screen as BuilderScreen & { template: "date" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<DateScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "coupon" && (
|
||
<CouponScreenConfig
|
||
screen={screen as BuilderScreen & { template: "coupon" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<CouponScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "form" && (
|
||
<FormScreenConfig
|
||
screen={screen as BuilderScreen & { template: "form" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<FormScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "list" && (
|
||
<ListScreenConfig
|
||
screen={screen as BuilderScreen & { template: "list" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<ListScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "email" && (
|
||
<EmailScreenConfig
|
||
screen={screen as BuilderScreen & { template: "email" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<EmailScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "loaders" && (
|
||
<LoadersScreenConfig
|
||
screen={screen as BuilderScreen & { template: "loaders" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<LoadersScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "soulmate" && (
|
||
<SoulmatePortraitScreenConfig
|
||
screen={screen as BuilderScreen & { template: "soulmate" }}
|
||
onUpdate={
|
||
onUpdate as (
|
||
updates: Partial<SoulmatePortraitScreenDefinition>
|
||
) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "trialPayment" && (
|
||
<TrialPaymentScreenConfig
|
||
screen={screen as BuilderScreen & { template: "trialPayment" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<TrialPaymentScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "specialOffer" && (
|
||
<SpecialOfferScreenConfig
|
||
screen={screen as BuilderScreen & { template: "specialOffer" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<SpecialOfferScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
{template === "trialChoice" && (
|
||
<TrialChoiceScreenConfig
|
||
screen={screen as BuilderScreen & { template: "trialChoice" }}
|
||
onUpdate={
|
||
onUpdate as (updates: Partial<TrialChoiceScreenDefinition>) => void
|
||
}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|