@upstart.gg/sdk
Version:
You can test the CLI without recompiling by running:
227 lines (215 loc) • 7.48 kB
text/typescript
import { Type, type Static } from "@sinclair/typebox";
import chroma from "chroma-js";
import { colorPalette } from "@upstart.gg/style-system/colors";
import { StringEnum } from "./utils/string-enum";
import { toLLMSchema } from "./utils/llm";
export const fontStacks = [
{ value: "system-ui", label: "System UI" },
{ value: "transitional", label: "Transitional" },
{ value: "old-style", label: "Old style" },
{ value: "humanist", label: "Humanist" },
{ value: "geometric-humanist", label: "Geometric humanist" },
{ value: "classical-humanist", label: "Classical humanist" },
{ value: "neo-grotesque", label: "Neo-grotesque" },
{ value: "monospace-slab-serif", label: "Monospace slab serif" },
{ value: "monospace-code", label: "Monospace code" },
{ value: "industrial", label: "Industrial" },
{ value: "rounded-sans", label: "Rounded sans" },
{ value: "slab-serif", label: "Slab serif" },
{ value: "antique", label: "Antique" },
{ value: "didone", label: "Didone" },
{ value: "handwritten", label: "Handwritten" },
];
const headingFont = Type.Object(
{
type: StringEnum(["stack", "theme", "google"], {
title: "Type of font",
description: "The type of font. Can be a font stack, a theme font or a Google font",
}),
family: Type.String({
title: "Family",
description: "The font family (eg. the name of the font)",
}),
},
{
title: "Headings font",
description: "Used for titles and headings",
},
);
const bodyFont = Type.Object(
{
type: StringEnum(["stack", "theme", "google"], {
title: "Type of font",
description: "The type of font. Can be a font stack, a theme font or a Google font",
}),
family: Type.String({
title: "Family",
description: "The font family (eg. the name of the font)",
}),
},
{
title: "Body font",
description: "Used for paragraphs and body text",
},
);
export const themeSchema = Type.Object({
id: Type.String({ title: "ID", description: "The unique identifier of the theme" }),
name: Type.String({ title: "Name", description: "The name of the theme" }),
description: Type.String({ title: "Description", description: "The description of the theme" }),
tags: Type.Array(Type.String({ title: "Tag" }), { title: "Tags", description: "The tags of the theme" }),
browserColorScheme: StringEnum(["light", "dark"], {
title: "Browser scheme",
description: "Color of browser-provided UI. Either 'light' or 'dark'",
}),
// Define the theme colors
colors: Type.Object(
{
primary: Type.String({
title: "Primary",
description: "The brand's primary color.",
"ai:instructions": "Use oklch() css notation.",
examples: ["oklch(0.62 0.241 354.308)"],
}),
secondary: Type.String({
title: "Secondary",
description: "The brand's second most used color",
"ai:instructions": "Use oklch() css notation.",
examples: ["oklch(0.65 0.22 185)"],
}),
accent: Type.String({
title: "Accent",
description: "The brand's least used color",
"ai:instructions": "Use oklch() css notation.",
examples: ["oklch(0.82 0.18 85)"],
}),
neutral: Type.String({
title: "Neutral",
description: "The base neutral color",
"ai:instructions": "Use oklch() css notation.",
examples: ["oklch(0.38 0.08 280)"],
}),
base100: Type.String({
title: "Base",
description:
"Base surface color of page, used for blank backgrounds. Should be white or near-white for light color-schemes, and black or near-black for dark color-schemes.",
"ai:instructions": "Use oklab() css notation.",
examples: ["oklch(0.99 0.008 92)"],
}),
base200: Type.String({
title: "Base 2",
description:
"Should be darker than base 100 but still light for light color-schemes, and lighter but still dark for dark color-schemes.",
"ai:instructions": "Use oklab() css notation.",
examples: ["oklch(0.97 0.01 85)"],
}),
base300: Type.String({
title: "Base 3",
description:
"3rd base color, should be darker than base 200 for light color-schemes, and lighter than base 200 for dark color-schemes.",
"ai:instructions": "Use oklab() css notation.",
examples: ["oklch(0.95 0.02 80)"],
}),
},
{
title: "Theme base colors",
description: "The base colors of the theme. Each theme must declare all these colors",
},
),
// Define the theme typography
typography: Type.Object({
base: Type.Number({
title: "Base font size",
description: "The base font size in pixels. It is safe to keep it as is.",
"ai:instructions": "A safe value is 16.",
}),
heading: headingFont,
body: bodyFont,
alternatives: Type.Array(
Type.Object({
body: bodyFont,
heading: headingFont,
}),
{
title: "Alternative fonts",
description: "Alternative fonts that can be suggested to the user. Takes the same shape",
},
),
}),
});
export type Theme = Static<typeof themeSchema>;
export const themesArray = Type.Array(themeSchema);
export type ThemesArray = Static<typeof themesArray>;
export type FontType = Theme["typography"]["body"];
export const defaultTheme: Theme = {
id: "_default_",
name: "default",
description: "Default Upstart theme",
tags: ["gradient", "vibrant", "modern", "creative", "dynamic", "artistic", "bold"],
browserColorScheme: "light",
colors: {
base100: "#FFF", // Warm white background
base200: "#F5F0E1", // Soft cream in hex
base300: "#F0E3D2", // Light warm gray in hex
primary: "#FF6F20", // A vibrant orange in hex
secondary: "#00BFFF", // A bright cyan in hex
accent: "#A4D65E", // A lively lime green in hex
neutral: "#B0B0B0", // A balanced gray in hex
},
typography: {
base: 16,
heading: { type: "stack", family: "system-ui" },
body: { type: "stack", family: "system-ui" },
alternatives: [],
},
};
export function isDefaultTheme(theme: Theme): boolean {
return theme.id === defaultTheme.id;
}
/**
* Process a theme, eventually fixing colors and translating them to oklch notations
* @param theme
*/
export function processTheme(theme: Theme): Theme {
return {
...theme,
typography: {
...theme.typography,
base: 16, // override any base size
},
colors: Object.entries(theme.colors).reduce(
(acc, [key, value]) => {
const fixedColor = fixOklchColor(value);
return {
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
...acc,
[key]: fixedColor,
};
},
{} as typeof theme.colors,
),
};
}
function fixOklchColor(color: string) {
const valid = chroma.valid(color);
if (valid) {
return color;
}
// Try to fix the color if it looks like oklch
const oklchRegex = /ok(lch|lab)\(([^)]+)\)/;
if (oklchRegex.test(color)) {
const withoutComma = color.replace(/,/g, " ");
if (chroma.valid(withoutComma)) {
return withoutComma;
}
}
// tailwind colors
if (/^([a-z]+)-([0-9]+)$/.test(color)) {
const [name, shade] = color.split("-");
// @ts-ignore
const twColor = colorPalette[name]?.[shade] as string | undefined;
if (twColor) {
return twColor;
}
}
return color;
}