150 lines
3.8 KiB
TypeScript
150 lines
3.8 KiB
TypeScript
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 = <T extends z.ZodTypeAny>(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 };
|