Files
buzz-sheet/lib/blocks/schemas.ts
T

284 lines
7.3 KiB
TypeScript

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<typeof comparisonChartSchema>;
export type ResponsiveLayout = z.output<typeof responsiveLayoutSchema>;