284 lines
7.3 KiB
TypeScript
284 lines
7.3 KiB
TypeScript
import { z } from "zod";
|
|
|
|
const uuid = z.string().uuid();
|
|
const shortText = z.string().trim().min(1).max(160);
|
|
const bodyText = z.string().max(20_000);
|
|
const anchor = z
|
|
.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(120)
|
|
.regex(/^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u)
|
|
.optional();
|
|
const safeLink = z
|
|
.string()
|
|
.trim()
|
|
.max(2_000)
|
|
.refine(
|
|
(value) => /^(https?:\/\/|\/|#|mailto:)/u.test(value),
|
|
"Links must use https, http, mailto, an anchor, or a same-origin path.",
|
|
);
|
|
|
|
export const responsiveLayoutSchema = z.strictObject({
|
|
width: z.enum(["content", "wide", "full"]).default("content"),
|
|
mobileSpan: z.number().int().min(1).max(4).default(4),
|
|
tabletSpan: z.number().int().min(1).max(8).default(8),
|
|
desktopSpan: z.number().int().min(1).max(12).default(12),
|
|
hideOnMobile: z.boolean().default(false),
|
|
});
|
|
|
|
const badgeSchema = z.strictObject({
|
|
label: shortText,
|
|
value: z.string().trim().max(160).optional(),
|
|
tone: z.enum(["neutral", "element", "rarity", "status"]).default("neutral"),
|
|
});
|
|
|
|
const profileSchema = z.strictObject({
|
|
eyebrow: z.string().trim().max(120).optional(),
|
|
title: shortText,
|
|
subtitle: z.string().trim().max(240).optional(),
|
|
summary: bodyText.optional(),
|
|
mediaId: uuid.nullable().optional(),
|
|
badges: z.array(badgeSchema).max(12).default([]),
|
|
});
|
|
|
|
const richTextNodeSchema = z.discriminatedUnion("type", [
|
|
z.strictObject({ type: z.literal("paragraph"), text: bodyText }),
|
|
z.strictObject({ type: z.literal("bulleted-list"), items: z.array(bodyText).max(50) }),
|
|
z.strictObject({ type: z.literal("numbered-list"), items: z.array(bodyText).max(50) }),
|
|
z.strictObject({ type: z.literal("quote"), text: bodyText }),
|
|
]);
|
|
|
|
const richTextSchema = z.strictObject({
|
|
content: z.array(richTextNodeSchema).max(200),
|
|
});
|
|
|
|
const headingSchema = z.strictObject({
|
|
text: shortText,
|
|
level: z.union([z.literal(2), z.literal(3), z.literal(4)]).default(2),
|
|
anchor,
|
|
});
|
|
|
|
const calloutSchema = z.strictObject({
|
|
title: z.string().trim().max(160).optional(),
|
|
body: bodyText,
|
|
tone: z.enum(["info", "tip", "warning", "danger"]).default("info"),
|
|
});
|
|
|
|
const statCardSchema = z.strictObject({
|
|
id: uuid,
|
|
label: shortText,
|
|
value: z.string().trim().max(160),
|
|
suffix: z.string().trim().max(40).optional(),
|
|
description: z.string().trim().max(320).optional(),
|
|
});
|
|
|
|
const statCardsSchema = z.strictObject({
|
|
title: z.string().trim().max(160).optional(),
|
|
items: z.array(statCardSchema).min(1).max(12),
|
|
});
|
|
|
|
const keyValueSchema = z.strictObject({
|
|
title: z.string().trim().max(160).optional(),
|
|
items: z
|
|
.array(
|
|
z.strictObject({
|
|
id: uuid,
|
|
label: shortText,
|
|
value: z.string().trim().max(500),
|
|
description: z.string().trim().max(1_000).optional(),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(40),
|
|
columns: z.union([z.literal(1), z.literal(2), z.literal(3)]).default(2),
|
|
});
|
|
|
|
const rankingSchema = z.strictObject({
|
|
title: shortText,
|
|
description: z.string().max(2_000).optional(),
|
|
dataSourceId: uuid,
|
|
labelField: shortText,
|
|
valueField: shortText,
|
|
detailField: z.string().trim().max(80).optional(),
|
|
badgeField: z.string().trim().max(80).optional(),
|
|
higherIsBetter: z.boolean().default(true),
|
|
});
|
|
|
|
const teamCompositionSchema = z.strictObject({
|
|
title: shortText,
|
|
dataSourceId: uuid,
|
|
teamField: shortText,
|
|
memberFields: z.array(shortText).min(1).max(8),
|
|
totalField: z.string().trim().max(80).optional(),
|
|
notesField: z.string().trim().max(80).optional(),
|
|
});
|
|
|
|
const rotationSchema = z.strictObject({
|
|
title: shortText,
|
|
description: z.string().max(2_000).optional(),
|
|
steps: z
|
|
.array(
|
|
z.strictObject({
|
|
id: uuid,
|
|
actor: z.string().trim().max(120).optional(),
|
|
action: shortText,
|
|
detail: z.string().max(2_000).optional(),
|
|
duration: z.string().trim().max(40).optional(),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(80),
|
|
});
|
|
|
|
const tableSchema = z.strictObject({
|
|
title: z.string().trim().max(160).optional(),
|
|
dataSourceId: uuid,
|
|
columns: z
|
|
.array(
|
|
z.strictObject({
|
|
field: shortText,
|
|
label: shortText,
|
|
align: z.enum(["start", "center", "end"]).default("start"),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(20),
|
|
});
|
|
|
|
export const comparisonChartSchema = z.strictObject({
|
|
title: z.string().trim().max(160).optional(),
|
|
description: z.string().max(2_000).optional(),
|
|
dataSourceId: uuid,
|
|
categoryField: shortText,
|
|
series: z
|
|
.array(
|
|
z.strictObject({
|
|
field: shortText,
|
|
label: shortText,
|
|
color: z.enum([
|
|
"chart-1",
|
|
"chart-2",
|
|
"chart-3",
|
|
"chart-4",
|
|
"chart-5",
|
|
"chart-6",
|
|
]),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(6),
|
|
presentation: z.enum([
|
|
"horizontal-bar",
|
|
"grouped-bar",
|
|
"stacked-bar",
|
|
"line",
|
|
]),
|
|
normalization: z.enum(["raw", "percent-of-total", "baseline-100"]),
|
|
sort: z.enum(["authored", "ascending", "descending"]),
|
|
showValues: z.boolean().default(true),
|
|
showLegend: z.boolean().default(true),
|
|
tableFallback: z.literal(true).default(true),
|
|
});
|
|
|
|
const imageSchema = z.strictObject({
|
|
mediaId: uuid,
|
|
alt: z.string().trim().min(1).max(500),
|
|
caption: z.string().max(1_000).optional(),
|
|
fit: z.enum(["cover", "contain"]).default("cover"),
|
|
});
|
|
|
|
const gallerySchema = z.strictObject({
|
|
title: z.string().trim().max(160).optional(),
|
|
images: z
|
|
.array(
|
|
z.strictObject({
|
|
mediaId: uuid,
|
|
alt: z.string().trim().min(1).max(500),
|
|
caption: z.string().max(1_000).optional(),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(24),
|
|
});
|
|
|
|
const tabsSchema = z.strictObject({
|
|
title: z.string().trim().max(160).optional(),
|
|
tabs: z
|
|
.array(
|
|
z.strictObject({
|
|
id: uuid,
|
|
label: shortText,
|
|
content: bodyText,
|
|
}),
|
|
)
|
|
.min(2)
|
|
.max(10),
|
|
});
|
|
|
|
const accordionSchema = z.strictObject({
|
|
title: z.string().trim().max(160).optional(),
|
|
items: z
|
|
.array(
|
|
z.strictObject({ id: uuid, title: shortText, content: bodyText }),
|
|
)
|
|
.min(1)
|
|
.max(30),
|
|
});
|
|
|
|
const sectionSchema = z.strictObject({
|
|
title: shortText,
|
|
description: z.string().max(2_000).optional(),
|
|
anchor,
|
|
});
|
|
|
|
const columnsSchema = z.strictObject({
|
|
columns: z.union([z.literal(2), z.literal(3), z.literal(4)]),
|
|
gap: z.enum(["compact", "normal", "relaxed"]).default("normal"),
|
|
});
|
|
|
|
const linksSchema = z.strictObject({
|
|
title: z.string().trim().max(160).optional(),
|
|
links: z
|
|
.array(
|
|
z.strictObject({
|
|
id: uuid,
|
|
label: shortText,
|
|
href: safeLink,
|
|
description: z.string().max(500).optional(),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(30),
|
|
});
|
|
|
|
const separatorSchema = z.strictObject({
|
|
label: z.string().trim().max(80).optional(),
|
|
});
|
|
|
|
export const blockSchemas = {
|
|
profile: profileSchema,
|
|
"rich-text": richTextSchema,
|
|
heading: headingSchema,
|
|
callout: calloutSchema,
|
|
"stat-cards": statCardsSchema,
|
|
"key-value": keyValueSchema,
|
|
ranking: rankingSchema,
|
|
"team-composition": teamCompositionSchema,
|
|
rotation: rotationSchema,
|
|
table: tableSchema,
|
|
"comparison-chart": comparisonChartSchema,
|
|
image: imageSchema,
|
|
gallery: gallerySchema,
|
|
tabs: tabsSchema,
|
|
accordion: accordionSchema,
|
|
section: sectionSchema,
|
|
columns: columnsSchema,
|
|
links: linksSchema,
|
|
separator: separatorSchema,
|
|
} as const;
|
|
|
|
export type KnownBlockType = keyof typeof blockSchemas;
|
|
export type ComparisonChartConfig = z.output<typeof comparisonChartSchema>;
|
|
export type ResponsiveLayout = z.output<typeof responsiveLayoutSchema>;
|