import * as 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; export type ResponsiveLayout = z.output;