689 lines
19 KiB
TypeScript
689 lines
19 KiB
TypeScript
import type {
|
||
DetailGenerationMode,
|
||
DetailUiGlobalStyle,
|
||
DetailUiProtocol,
|
||
DetailUiScreenInput,
|
||
DetailUiScreenProtocol,
|
||
DetailUiScreenReviewPatch,
|
||
} from "@/lib/detail-ui-protocol";
|
||
import type {
|
||
UiSurfaceComponent,
|
||
UiSurfacePatch,
|
||
UiSurfaceSnapshot,
|
||
} from "@/lib/ui-surface-protocol";
|
||
|
||
function createDetailSurfaceId(params: {
|
||
generationMode: DetailGenerationMode;
|
||
screenCount: number;
|
||
}) {
|
||
return `detail-surface-${params.generationMode}-${params.screenCount}-790x1300`;
|
||
}
|
||
|
||
function createRootComponent(): UiSurfaceComponent {
|
||
return {
|
||
id: "detail-page-root",
|
||
type: "detail_page_root",
|
||
parentId: null,
|
||
props: {
|
||
role: "detail_page",
|
||
},
|
||
};
|
||
}
|
||
|
||
type DetailSurfaceLayoutVariant = "hero_left" | "hero_right" | "split_bottom";
|
||
|
||
function splitBodyCopyIntoHighlights(copy?: string) {
|
||
const segments = String(copy || "")
|
||
.split(/[。\n!!??;;]+/g)
|
||
.map((segment) => segment.replace(/\s+/g, " ").trim())
|
||
.filter(Boolean);
|
||
|
||
if (segments.length === 0) return [];
|
||
if (segments.length <= 3) return segments;
|
||
|
||
return [
|
||
segments.slice(0, 2).join(" · "),
|
||
segments.slice(2, 4).join(" · "),
|
||
segments.slice(4).join(" · "),
|
||
].filter(Boolean);
|
||
}
|
||
|
||
function pickScreenLayoutVariant(params: {
|
||
screenIndex: number;
|
||
screenType?: string;
|
||
}): DetailSurfaceLayoutVariant {
|
||
if (params.screenIndex === 1 || params.screenType === "B") {
|
||
return "hero_left";
|
||
}
|
||
if (params.screenType === "A") {
|
||
return "hero_right";
|
||
}
|
||
return "split_bottom";
|
||
}
|
||
|
||
function buildScreenBadgeLabel(params: {
|
||
screenIndex: number;
|
||
screenType?: string;
|
||
objective?: string;
|
||
}) {
|
||
const normalizedObjective = String(params.objective || "").replace(/\s+/g, " ").trim();
|
||
if (normalizedObjective) return normalizedObjective.slice(0, 18);
|
||
|
||
if (params.screenType === "A") return "技术证明";
|
||
if (params.screenType === "B") return "场景共鸣";
|
||
if (params.screenType === "C") return "细节证明";
|
||
return `第 ${params.screenIndex} 屏`;
|
||
}
|
||
|
||
function createStyledSurfaceTextComponent(params: {
|
||
id: string;
|
||
type: "detail_card" | "detail_badge" | "detail_text_block";
|
||
parentId: string;
|
||
frame: { x: number; y: number; width: number; height: number };
|
||
zIndex?: number;
|
||
content?: string;
|
||
bindingPath?: string;
|
||
role: string;
|
||
style: Record<string, unknown>;
|
||
metadata?: Record<string, unknown>;
|
||
}) {
|
||
return {
|
||
id: params.id,
|
||
type: params.type,
|
||
parentId: params.parentId,
|
||
props: {
|
||
role: params.role,
|
||
content: params.content || "",
|
||
},
|
||
bindings: params.bindingPath
|
||
? {
|
||
content: {
|
||
kind: "text" as const,
|
||
path: params.bindingPath,
|
||
},
|
||
}
|
||
: undefined,
|
||
layout: {
|
||
frame: params.frame,
|
||
zIndex: params.zIndex || 1,
|
||
},
|
||
style: params.style,
|
||
metadata: params.metadata,
|
||
} satisfies UiSurfaceComponent;
|
||
}
|
||
|
||
function createSectionComponent(params: {
|
||
id: string;
|
||
parentId: string;
|
||
role: string;
|
||
frame: { x: number; y: number; width: number; height: number };
|
||
zIndex?: number;
|
||
metadata?: Record<string, unknown>;
|
||
}) {
|
||
return {
|
||
id: params.id,
|
||
type: "detail_section" as const,
|
||
parentId: params.parentId,
|
||
props: {
|
||
role: params.role,
|
||
},
|
||
layout: {
|
||
frame: params.frame,
|
||
zIndex: params.zIndex || 1,
|
||
},
|
||
metadata: params.metadata,
|
||
} satisfies UiSurfaceComponent;
|
||
}
|
||
|
||
function buildSectionedTextComponents(params: {
|
||
screenId: string;
|
||
textStackId: string;
|
||
screenIndex: number;
|
||
screenSize: { width: number; height: number };
|
||
screen: Pick<
|
||
DetailUiScreenInput,
|
||
"title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote"
|
||
>;
|
||
screenProtocol?: DetailUiScreenProtocol;
|
||
}) {
|
||
const safeArea = params.screenProtocol?.safeArea || {
|
||
top: 64,
|
||
bottom: 360,
|
||
left: 56,
|
||
right: 56,
|
||
};
|
||
const contentWidth = Math.max(
|
||
260,
|
||
Math.round(params.screenSize.width - safeArea.left - safeArea.right),
|
||
);
|
||
const variant = pickScreenLayoutVariant({
|
||
screenIndex: params.screenIndex,
|
||
screenType: params.screen.screenType,
|
||
});
|
||
const badgeLabel = buildScreenBadgeLabel({
|
||
screenIndex: params.screenIndex,
|
||
screenType: params.screen.screenType,
|
||
objective: params.screen.objective,
|
||
});
|
||
const bodyHighlights = splitBodyCopyIntoHighlights(params.screen.copy);
|
||
|
||
const titleBlock =
|
||
params.screenProtocol?.blocks.find((block) => block.type === "title") || null;
|
||
const subtitleBlock =
|
||
params.screenProtocol?.blocks.find((block) => block.type === "subtitle") || null;
|
||
const bodyBlock =
|
||
params.screenProtocol?.blocks.find((block) => block.type === "body") || null;
|
||
|
||
const cardX =
|
||
variant === "hero_right"
|
||
? Math.round(params.screenSize.width - safeArea.right - Math.min(430, contentWidth))
|
||
: safeArea.left;
|
||
const cardWidth = Math.min(430, contentWidth);
|
||
const titleY = safeArea.top + 42;
|
||
const subtitleY = titleY + Math.max(124, Math.round(Number(titleBlock?.frame.height || 132))) + 14;
|
||
|
||
const components: UiSurfaceComponent[] = [];
|
||
|
||
components.push(
|
||
createSectionComponent({
|
||
id: `${params.screenId}-section-hero`,
|
||
parentId: params.textStackId,
|
||
role: "hero_section",
|
||
frame: {
|
||
x: cardX,
|
||
y: safeArea.top,
|
||
width: cardWidth,
|
||
height: Math.min(360, params.screenSize.height - safeArea.top - 120),
|
||
},
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
variant,
|
||
},
|
||
}),
|
||
);
|
||
|
||
components.push(
|
||
createStyledSurfaceTextComponent({
|
||
id: `${params.screenId}-badge`,
|
||
type: "detail_badge",
|
||
parentId: `${params.screenId}-section-hero`,
|
||
frame: {
|
||
x: cardX,
|
||
y: safeArea.top,
|
||
width: Math.min(156, Math.max(104, badgeLabel.length * 18)),
|
||
height: 34,
|
||
},
|
||
zIndex: 3,
|
||
content: badgeLabel,
|
||
role: "screen_badge",
|
||
style: {
|
||
fontSize: 16,
|
||
fontWeight: "700",
|
||
color: "#f8fafc",
|
||
textAlign: "center",
|
||
lineHeight: 1.15,
|
||
letterSpacing: 0.4,
|
||
backgroundColor: "rgba(15,23,42,0.82)",
|
||
borderColor: "rgba(255,255,255,0.18)",
|
||
borderWidth: 1,
|
||
borderRadius: 999,
|
||
padding: 8,
|
||
boxShadow: "0 10px 28px rgba(15,23,42,0.20)",
|
||
backdropBlur: 8,
|
||
textTransform: "uppercase",
|
||
},
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
componentRole: "badge",
|
||
},
|
||
}),
|
||
);
|
||
|
||
components.push(
|
||
createStyledSurfaceTextComponent({
|
||
id: `${params.screenId}-title-card`,
|
||
type: "detail_card",
|
||
parentId: `${params.screenId}-section-hero`,
|
||
frame: {
|
||
x: cardX,
|
||
y: titleY,
|
||
width: cardWidth,
|
||
height: Math.max(132, Math.round(Number(titleBlock?.frame.height || 132) + 34)),
|
||
},
|
||
zIndex: 2,
|
||
bindingPath: `/screens/${params.screenIndex - 1}/title`,
|
||
role: "title_card",
|
||
style: {
|
||
fontFamily: titleBlock?.style.fontFamily || undefined,
|
||
fontSize: Math.max(34, Number(titleBlock?.style.fontSize || 52)),
|
||
fontWeight: titleBlock?.style.fontWeight || "800",
|
||
color: "#0f172a",
|
||
textAlign: "left",
|
||
lineHeight: 1.08,
|
||
letterSpacing: -0.8,
|
||
backgroundColor: "rgba(255,255,255,0.86)",
|
||
borderColor: "rgba(255,255,255,0.38)",
|
||
borderWidth: 1,
|
||
borderRadius: 28,
|
||
padding: 24,
|
||
boxShadow: "0 22px 58px rgba(15,23,42,0.18)",
|
||
backdropBlur: 14,
|
||
},
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
componentRole: "title",
|
||
},
|
||
}),
|
||
);
|
||
|
||
if (params.screen.subtitle) {
|
||
components.push(
|
||
createStyledSurfaceTextComponent({
|
||
id: `${params.screenId}-subtitle-card`,
|
||
type: "detail_card",
|
||
parentId: `${params.screenId}-section-hero`,
|
||
frame: {
|
||
x: cardX,
|
||
y: subtitleY,
|
||
width: Math.min(cardWidth, 360),
|
||
height: Math.max(72, Math.round(Number(subtitleBlock?.frame.height || 64) + 26)),
|
||
},
|
||
zIndex: 2,
|
||
bindingPath: `/screens/${params.screenIndex - 1}/subtitle`,
|
||
role: "subtitle_card",
|
||
style: {
|
||
fontFamily: subtitleBlock?.style.fontFamily || undefined,
|
||
fontSize: Math.max(22, Number(subtitleBlock?.style.fontSize || 28)),
|
||
fontWeight: subtitleBlock?.style.fontWeight || "700",
|
||
color: "#f8fafc",
|
||
textAlign: "left",
|
||
lineHeight: 1.18,
|
||
backgroundColor:
|
||
variant === "hero_right" ? "rgba(37,99,235,0.82)" : "rgba(30,41,59,0.78)",
|
||
borderColor: "rgba(255,255,255,0.18)",
|
||
borderWidth: 1,
|
||
borderRadius: 22,
|
||
padding: 18,
|
||
boxShadow: "0 18px 42px rgba(15,23,42,0.18)",
|
||
backdropBlur: 12,
|
||
},
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
componentRole: "subtitle",
|
||
},
|
||
}),
|
||
);
|
||
}
|
||
|
||
const supportingSectionY =
|
||
variant === "split_bottom" ? Math.round(params.screenSize.height * 0.72) : subtitleY + 96;
|
||
const supportingSectionHeight =
|
||
variant === "split_bottom"
|
||
? Math.min(320, params.screenSize.height - supportingSectionY - 80)
|
||
: 240;
|
||
|
||
components.push(
|
||
createSectionComponent({
|
||
id: `${params.screenId}-section-supporting`,
|
||
parentId: params.textStackId,
|
||
role: variant === "split_bottom" ? "supporting_bottom_section" : "supporting_grid",
|
||
frame: {
|
||
x: safeArea.left,
|
||
y: supportingSectionY,
|
||
width: contentWidth,
|
||
height: supportingSectionHeight,
|
||
},
|
||
zIndex: 1,
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
variant,
|
||
},
|
||
}),
|
||
);
|
||
|
||
const supportingCards =
|
||
bodyHighlights.length > 0
|
||
? bodyHighlights.slice(0, 3)
|
||
: [String(params.screen.copy || "").trim()].filter(Boolean);
|
||
const cardGap = 14;
|
||
const supportColumns = variant === "split_bottom" ? Math.min(3, supportingCards.length) : Math.min(2, Math.max(1, supportingCards.length));
|
||
const supportCardWidth = Math.floor(
|
||
(contentWidth - cardGap * Math.max(0, supportColumns - 1)) / supportColumns,
|
||
);
|
||
|
||
supportingCards.forEach((highlight, index) => {
|
||
const column = supportColumns === 0 ? 0 : index % supportColumns;
|
||
const row = supportColumns === 0 ? 0 : Math.floor(index / supportColumns);
|
||
const x = safeArea.left + column * (supportCardWidth + cardGap);
|
||
const y = supportingSectionY + row * 116;
|
||
components.push(
|
||
createStyledSurfaceTextComponent({
|
||
id: `${params.screenId}-support-card-${index + 1}`,
|
||
type: "detail_card",
|
||
parentId: `${params.screenId}-section-supporting`,
|
||
frame: {
|
||
x,
|
||
y,
|
||
width: supportCardWidth,
|
||
height: variant === "split_bottom" ? 102 : 96,
|
||
},
|
||
zIndex: 2,
|
||
content: highlight,
|
||
role: "support_card",
|
||
style: {
|
||
fontSize: bodyBlock ? Math.max(18, Number(bodyBlock.style.fontSize || 22)) : 20,
|
||
fontWeight: "600",
|
||
color: "#111827",
|
||
textAlign: "left",
|
||
lineHeight: 1.32,
|
||
backgroundColor:
|
||
variant === "split_bottom"
|
||
? "rgba(255,255,255,0.92)"
|
||
: "rgba(248,250,252,0.82)",
|
||
borderColor:
|
||
index === 0 ? "rgba(59,130,246,0.42)" : "rgba(148,163,184,0.28)",
|
||
borderWidth: 1,
|
||
borderRadius: 24,
|
||
padding: 18,
|
||
boxShadow: "0 18px 42px rgba(15,23,42,0.12)",
|
||
backdropBlur: 12,
|
||
},
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
componentRole: "support_card",
|
||
},
|
||
}),
|
||
);
|
||
});
|
||
|
||
if (params.screen.layoutNote) {
|
||
const noteY = Math.min(
|
||
params.screenSize.height - 116,
|
||
supportingSectionY + supportingSectionHeight - 74,
|
||
);
|
||
components.push(
|
||
createStyledSurfaceTextComponent({
|
||
id: `${params.screenId}-proof-card`,
|
||
type: "detail_card",
|
||
parentId: `${params.screenId}-section-supporting`,
|
||
frame: {
|
||
x:
|
||
variant === "hero_right"
|
||
? Math.round(params.screenSize.width - safeArea.right - Math.min(260, contentWidth * 0.46))
|
||
: safeArea.left,
|
||
y: noteY,
|
||
width: Math.min(260, contentWidth * 0.46),
|
||
height: 62,
|
||
},
|
||
zIndex: 2,
|
||
content: String(params.screen.layoutNote || "").replace(/\s+/g, " ").trim(),
|
||
role: "proof_chip",
|
||
style: {
|
||
fontSize: 16,
|
||
fontWeight: "600",
|
||
color: "#1e293b",
|
||
textAlign: "left",
|
||
lineHeight: 1.22,
|
||
backgroundColor: "rgba(255,255,255,0.88)",
|
||
borderColor: "rgba(15,23,42,0.10)",
|
||
borderWidth: 1,
|
||
borderRadius: 18,
|
||
padding: 14,
|
||
boxShadow: "0 14px 36px rgba(15,23,42,0.12)",
|
||
backdropBlur: 10,
|
||
},
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
componentRole: "proof_chip",
|
||
},
|
||
}),
|
||
);
|
||
}
|
||
|
||
return components;
|
||
}
|
||
|
||
function createScreenSubtree(params: {
|
||
screenIndex: number;
|
||
screen: Pick<
|
||
DetailUiScreenInput,
|
||
"title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote"
|
||
>;
|
||
screenSize: { width: number; height: number };
|
||
screenProtocol?: DetailUiScreenProtocol;
|
||
rootId?: string;
|
||
}) {
|
||
const screenId = params.rootId || `detail-screen-${params.screenIndex}`;
|
||
const backgroundId = `${screenId}-background`;
|
||
const textStackId = `${screenId}-text-stack`;
|
||
|
||
const components: UiSurfaceComponent[] = [
|
||
{
|
||
id: screenId,
|
||
type: "detail_screen",
|
||
parentId: "detail-page-root",
|
||
props: {
|
||
screenIndex: params.screenIndex,
|
||
screenType: params.screen.screenType || "",
|
||
objective: params.screen.objective || "",
|
||
layoutNote: params.screen.layoutNote || "",
|
||
},
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
},
|
||
},
|
||
{
|
||
id: backgroundId,
|
||
type: "detail_background_image",
|
||
parentId: screenId,
|
||
slot: "background",
|
||
props: {
|
||
screenIndex: params.screenIndex,
|
||
},
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
},
|
||
},
|
||
{
|
||
id: textStackId,
|
||
type: "detail_text_stack",
|
||
parentId: screenId,
|
||
slot: "content",
|
||
props: {
|
||
screenIndex: params.screenIndex,
|
||
safeArea: params.screenProtocol?.safeArea || null,
|
||
},
|
||
metadata: {
|
||
screenIndex: params.screenIndex,
|
||
},
|
||
},
|
||
];
|
||
|
||
components.push(
|
||
...buildSectionedTextComponents({
|
||
screenId,
|
||
textStackId,
|
||
screenIndex: params.screenIndex,
|
||
screenSize: params.screenSize,
|
||
screen: params.screen,
|
||
screenProtocol: params.screenProtocol,
|
||
}),
|
||
);
|
||
|
||
return components;
|
||
}
|
||
|
||
function buildDetailSurfaceDataModel(params: {
|
||
summary?: string;
|
||
screens: DetailUiScreenInput[];
|
||
globalStyle?: DetailUiGlobalStyle;
|
||
}) {
|
||
return {
|
||
summary: params.summary || "",
|
||
globalStyle: params.globalStyle || {},
|
||
screens: params.screens.map((screen) => ({
|
||
index: screen.index,
|
||
title: String(screen.title || "").trim(),
|
||
subtitle: String(screen.subtitle || "").trim(),
|
||
copy: String(screen.copy || "").trim(),
|
||
screenType: screen.screenType || "",
|
||
objective: screen.objective || "",
|
||
layoutNote: screen.layoutNote || "",
|
||
})),
|
||
};
|
||
}
|
||
|
||
export function buildDetailUiSurfaceSnapshot(params: {
|
||
surfaceId?: string;
|
||
generationMode: DetailGenerationMode;
|
||
screenSize: { width: number; height: number };
|
||
summary?: string;
|
||
globalStyle?: DetailUiGlobalStyle;
|
||
screens: DetailUiScreenInput[];
|
||
uiProtocol?: DetailUiProtocol;
|
||
uiValidation?: {
|
||
passed: boolean;
|
||
issues: string[];
|
||
};
|
||
}) {
|
||
const surfaceId =
|
||
params.surfaceId ||
|
||
createDetailSurfaceId({
|
||
generationMode: params.generationMode,
|
||
screenCount: params.screens.length,
|
||
});
|
||
|
||
const components: UiSurfaceComponent[] = [createRootComponent()];
|
||
params.screens.forEach((screen) => {
|
||
const screenProtocol = params.uiProtocol?.screens.find(
|
||
(item) => item.index === screen.index,
|
||
);
|
||
components.push(
|
||
...createScreenSubtree({
|
||
screenIndex: screen.index,
|
||
screen,
|
||
screenSize: params.screenSize,
|
||
screenProtocol,
|
||
}),
|
||
);
|
||
});
|
||
|
||
return {
|
||
version: "ui-surface/v1",
|
||
surfaceId,
|
||
surfaceType: "detail_page",
|
||
generationMode: params.generationMode,
|
||
rootComponentId: "detail-page-root",
|
||
screenSize: params.screenSize,
|
||
components,
|
||
dataModel: buildDetailSurfaceDataModel({
|
||
summary: params.summary,
|
||
screens: params.screens,
|
||
globalStyle: params.globalStyle,
|
||
}),
|
||
validation: params.uiValidation
|
||
? {
|
||
passed: params.uiValidation.passed,
|
||
issues: params.uiValidation.issues,
|
||
source: "plan",
|
||
}
|
||
: undefined,
|
||
metadata: {
|
||
detailScreenCount: params.screens.length,
|
||
},
|
||
} satisfies UiSurfaceSnapshot;
|
||
}
|
||
|
||
export function buildDetailUiSurfacePatchFromReview(params: {
|
||
surfaceId: string;
|
||
screenIndex: number;
|
||
screen: Pick<
|
||
DetailUiScreenInput,
|
||
"index" | "title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote"
|
||
>;
|
||
uiPatch?: DetailUiScreenReviewPatch;
|
||
uiValidation?: {
|
||
passed: boolean;
|
||
issues: string[];
|
||
};
|
||
}) {
|
||
if (!params.uiPatch) return null;
|
||
|
||
const screenId = `detail-screen-${params.screenIndex}`;
|
||
const textStackId = `${screenId}-text-stack`;
|
||
const replacementComponents = createScreenSubtree({
|
||
screenIndex: params.screenIndex,
|
||
screen: params.screen,
|
||
screenSize: {
|
||
width: 790,
|
||
height: 1300,
|
||
},
|
||
screenProtocol: params.uiPatch.recommendedScreen,
|
||
rootId: screenId,
|
||
}).filter(
|
||
(component) =>
|
||
component.id !== screenId &&
|
||
component.id !== `${screenId}-background`,
|
||
);
|
||
|
||
return {
|
||
version: "ui-surface-patch/v1",
|
||
surfaceId: params.surfaceId,
|
||
surfaceType: "detail_page",
|
||
operations: [
|
||
{
|
||
type: "replace_component_subtree",
|
||
componentId: textStackId,
|
||
components: replacementComponents,
|
||
},
|
||
{
|
||
type: "set_data",
|
||
updates: [
|
||
{
|
||
path: `/screens/${params.screenIndex - 1}/title`,
|
||
value: String(params.screen.title || "").trim(),
|
||
},
|
||
{
|
||
path: `/screens/${params.screenIndex - 1}/subtitle`,
|
||
value: String(params.screen.subtitle || "").trim(),
|
||
},
|
||
{
|
||
path: `/screens/${params.screenIndex - 1}/copy`,
|
||
value: String(params.screen.copy || "").trim(),
|
||
},
|
||
{
|
||
path: `/screens/${params.screenIndex - 1}/layoutNote`,
|
||
value: String(params.screen.layoutNote || "").trim(),
|
||
},
|
||
],
|
||
},
|
||
...(params.uiValidation
|
||
? [
|
||
{
|
||
type: "set_validation" as const,
|
||
validation: {
|
||
passed: params.uiValidation.passed,
|
||
issues: params.uiValidation.issues,
|
||
source: "review" as const,
|
||
},
|
||
},
|
||
]
|
||
: []),
|
||
],
|
||
} satisfies UiSurfacePatch;
|
||
}
|
||
|
||
export function findDetailSurfaceScreenComponent(
|
||
snapshot: UiSurfaceSnapshot | null | undefined,
|
||
screenIndex: number,
|
||
) {
|
||
if (!snapshot) return null;
|
||
return (
|
||
snapshot.components.find(
|
||
(component) =>
|
||
component.type === "detail_screen" &&
|
||
Number(component.props?.screenIndex || 0) === screenIndex,
|
||
) || null
|
||
);
|
||
}
|