import { z } from "zod"; const hexColor = z.string().regex(/^#(?:[\da-f]{3}|[\da-f]{6})$/i); const finite = z.number().finite(); const positive = finite.positive(); const shade = z.union([ z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5), z.literal(6), z.literal(7), z.literal(8), z.literal(9), ]); const scale = (value: T) => z.object({ xs: value, sm: value, md: value, lg: value, xl: value }).strict(); const role = z .object({ color: z.string().min(1), shade: shade.nullable(), description: z.string().min(1), }) .strict(); const heading = z.object({ fontSize: positive, lineHeight: positive }).strict(); const shadow = z .object({ offsetX: finite, offsetY: finite, blur: finite.nonnegative(), spread: finite, color: z.enum(["white", "black"]), opacity: finite.min(0).max(1), }) .strict(); const designTokensSchema = z .object({ formatVersion: z.literal(1), description: z.string().min(1), color: z .object({ base: z.object({ white: hexColor, black: hexColor }).strict(), palettes: z.record( z .string() .regex(/^[a-z][a-z0-9-]*$/) .refine( (name) => name !== "dimmed" && name !== "bright", "Palette name is reserved by Mantine", ), z.array(hexColor).length(10), ), primaryShade: z.object({ light: shade, dark: shade }).strict(), roles: z.record(z.string().regex(/^[a-z][a-z0-9-]*$/), role), }) .strict(), typography: z .object({ fontFamily: z.string().min(1), fontFamilyMonospace: z.string().min(1), unit: z.literal("px"), fontSizes: scale(positive), fontWeights: z .object({ regular: finite.min(1).max(1000), medium: finite.min(1).max(1000), bold: finite.min(1).max(1000), }) .strict(), lineHeights: scale(positive), headings: z .object({ h1: heading, h2: heading, h3: heading, h4: heading, h5: heading, h6: heading, }) .strict(), }) .strict(), spacing: z .object({ unit: z.literal("px"), values: scale(finite.nonnegative()) }) .strict(), radius: z .object({ unit: z.literal("px"), default: z.enum(["xs", "sm", "md", "lg", "xl"]), values: scale(finite.nonnegative()), }) .strict(), shadows: z .object({ unit: z.literal("px"), values: scale(z.array(shadow).min(1)) }) .strict(), breakpoints: z .object({ unit: z.literal("em"), values: scale(positive) }) .strict(), }) .strict() .superRefine((tokens, context) => { const { base, palettes, roles } = tokens.color; for (const required of ["canvas", "text", "primary"]) { if (!Object.hasOwn(roles, required)) { context.addIssue({ code: "custom", path: ["color", "roles", required], message: "Required semantic role is missing", }); } } for (const [name, value] of Object.entries(roles)) { const isPalette = Object.hasOwn(palettes, value.color); const isBase = Object.hasOwn(base, value.color); if ((!isPalette && !isBase) || (isBase && value.shade !== null)) { context.addIssue({ code: "custom", path: ["color", "roles", name], message: "Role must reference an existing palette shade or an unshaded base color", }); } } if ( roles.primary && (!Object.hasOwn(palettes, roles.primary.color) || roles.primary.shade !== null) ) { context.addIssue({ code: "custom", path: ["color", "roles", "primary"], message: "Primary must reference a palette with the default shade", }); } for (const name of Object.keys(base)) { if (Object.hasOwn(palettes, name)) { context.addIssue({ code: "custom", path: ["color", "palettes", name], message: "Palette name conflicts with a base color", }); } } }); export { designTokensSchema };