| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { type Static, Type } from "typebox"; |
| import { Compile } from "typebox/compile"; |
|
|
| const ColorValueSchema = Type.Union([ |
| Type.String(), |
| Type.Integer({ minimum: 0, maximum: 255 }), |
| ]); |
|
|
| const ThemeJsonSchema = Type.Object({ |
| $schema: Type.Optional(Type.String()), |
| name: Type.String(), |
| vars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)), |
| colors: Type.Object({ |
| |
| accent: ColorValueSchema, |
| border: ColorValueSchema, |
| borderAccent: ColorValueSchema, |
| borderMuted: ColorValueSchema, |
| success: ColorValueSchema, |
| error: ColorValueSchema, |
| warning: ColorValueSchema, |
| muted: ColorValueSchema, |
| dim: ColorValueSchema, |
| text: ColorValueSchema, |
| thinkingText: ColorValueSchema, |
| |
| scrollbarTrack: Type.Optional(ColorValueSchema), |
| scrollbarThumb: Type.Optional(ColorValueSchema), |
| |
| selectedBg: ColorValueSchema, |
| searchMatchBg: Type.Optional(ColorValueSchema), |
| searchMatchText: Type.Optional(ColorValueSchema), |
| userMessageBg: ColorValueSchema, |
| userMessageText: ColorValueSchema, |
| customMessageBg: ColorValueSchema, |
| customMessageText: ColorValueSchema, |
| customMessageLabel: ColorValueSchema, |
| toolPendingBg: ColorValueSchema, |
| toolSuccessBg: ColorValueSchema, |
| toolErrorBg: ColorValueSchema, |
| toolTitle: ColorValueSchema, |
| toolOutput: ColorValueSchema, |
| |
| mdHeading: ColorValueSchema, |
| mdLink: ColorValueSchema, |
| mdLinkUrl: ColorValueSchema, |
| mdCode: ColorValueSchema, |
| mdCodeBlock: ColorValueSchema, |
| mdCodeBlockBorder: ColorValueSchema, |
| mdQuote: ColorValueSchema, |
| mdQuoteBorder: ColorValueSchema, |
| mdHr: ColorValueSchema, |
| mdListBullet: ColorValueSchema, |
| |
| toolDiffAdded: ColorValueSchema, |
| toolDiffRemoved: ColorValueSchema, |
| toolDiffContext: ColorValueSchema, |
| |
| syntaxComment: ColorValueSchema, |
| syntaxKeyword: ColorValueSchema, |
| syntaxFunction: ColorValueSchema, |
| syntaxVariable: ColorValueSchema, |
| syntaxString: ColorValueSchema, |
| syntaxNumber: ColorValueSchema, |
| syntaxType: ColorValueSchema, |
| syntaxOperator: ColorValueSchema, |
| syntaxPunctuation: ColorValueSchema, |
| |
| thinkingOff: ColorValueSchema, |
| thinkingMinimal: ColorValueSchema, |
| thinkingLow: ColorValueSchema, |
| thinkingMedium: ColorValueSchema, |
| thinkingHigh: ColorValueSchema, |
| thinkingXhigh: ColorValueSchema, |
| thinkingMax: Type.Optional(ColorValueSchema), |
| |
| bashMode: ColorValueSchema, |
| }), |
| export: Type.Optional( |
| Type.Object({ |
| pageBg: Type.Optional(ColorValueSchema), |
| cardBg: Type.Optional(ColorValueSchema), |
| infoBg: Type.Optional(ColorValueSchema), |
| }), |
| ), |
| }); |
|
|
| const compiledThemeSchema = Compile(ThemeJsonSchema); |
|
|
| export type ThemeColorValue = Static<typeof ColorValueSchema>; |
| export type ValidatedThemeJson = Static<typeof ThemeJsonSchema>; |
|
|
| |
| export function validateThemeJson(label: string, json: unknown): ValidatedThemeJson { |
| if (!compiledThemeSchema.Check(json)) { |
| const errors = Array.from(compiledThemeSchema.Errors(json)); |
| const missingColors = new Set<string>(); |
| const otherErrors: string[] = []; |
|
|
| for (const error of errors) { |
| if (error.keyword === "required" && error.instancePath === "/colors") { |
| const requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties; |
| for (const requiredProperty of requiredProperties ?? []) { |
| missingColors.add(requiredProperty); |
| } |
| continue; |
| } |
|
|
| const path = error.instancePath || "/"; |
| otherErrors.push(` - ${path}: ${error.message}`); |
| } |
|
|
| let errorMessage = `Invalid theme "${label}":\n`; |
| if (missingColors.size > 0) { |
| errorMessage += "\nMissing required color tokens:\n"; |
| errorMessage += Array.from(missingColors) |
| .sort() |
| .map((color) => ` - ${color}`) |
| .join("\n"); |
| errorMessage += '\n\nPlease add these colors to your theme\'s "colors" object.'; |
| errorMessage += "\nSee the built-in themes (dark.json, light.json) for reference values."; |
| } |
| if (otherErrors.length > 0) { |
| errorMessage += `\n\nOther errors:\n${otherErrors.join("\n")}`; |
| } |
|
|
| throw new Error(errorMessage); |
| } |
|
|
| const themeJson = json as ValidatedThemeJson; |
| if (themeJson.name.includes("/")) { |
| throw new Error( |
| `Invalid theme name "${themeJson.name}": theme names cannot contain "/" because it is reserved for automatic light/dark theme settings.`, |
| ); |
| } |
| return themeJson; |
| } |
|
|