Да, вы можете описать тему на английском и получить работающую дизайн-систему. Подвох в том, что описание на английском — это не промпт. Это исходный файл. И как любой исходный файл, ему нужен компилятор, система типов и тесты.
Если вы будете относиться к нему как к запросу чат-бота, во вторник вы получите одну цветовую палитру, а в понедельник — другую. Если вы будете относиться к нему как к DSL с ограниченным контекстом и строгой схемой, вы получите воспроизводимые токены, которые сможет потреблять ваша дизайн-система.
Дизайн-токены — это проблема синхронизации, а не творческая задача
Большинство команд не испытывают трудностей с выбором цветов. Они не могут сохранить консистентность цветов между Figma, CSS-переменными и библиотеками компонентов. Типичный рабочий процесс выглядит так: дизайнер обновляет hex-код в гайдлайне, разработчик копирует его в JSON-файл, другой разработчик ссылается на неверный ключ в React-компоненте, и через три месяца в одном месте у вас #1a1a2e, а в другом — #1b1b2f.
Настоящая боль — это передача. Figma — это не код. JSON — это не инструмент дизайна. Английский язык находится посередине. Это единственный формат, который и дизайнеры, и разработчики могут прочитать без обучения.
Вопрос не в том, может ли LLM превратить английский в hex-коды. Вопрос в том, можно ли сделать этот процесс достаточно надёжным, чтобы запускать его в CI.
Как на самом деле работает theme-to-code
Архитектура проста. Вы пишете короткий манифест темы на простом английском. Скрипт передаёт его LLM с ограниченным форматом вывода. LLM возвращает структурированные токены. Вы записываете эти токены на диск, проверяете типы и коммитите их.
Вот пайплайн:
- Манифест: Файл
.txtили.md, описывающий тему прозой. - Схема: Определение Zod (или JSON Schema), которое должен вернуть LLM.
- Компилятор: Скрипт, который читает манифест, вызывает LLM со структурированным выводом и записывает
tokens.json. - Ворота: Тест, который падает, если сгенерированные токены отклоняются от снапшота или нарушают ограничения.
Ключ — в третьем шаге. Вы не просите модель «сгенерировать тёмную тему». Вы просите её заполнить схему. Это превращает творческое письмо в задачу извлечения структурированных данных, с которой LLM справляются значительно лучше.
Рабочий компилятор на TypeScript
Вот минимальная, но полная реализация. Она читает описание темы, вызывает OpenAI со схемой Zod и записывает результат в JSON-файл.
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import OpenAI from "openai";
// 1. Define the bounded context: only these tokens exist
const TokenSchema = z.object({
name: z.string(),
colors: z.object({
background: z.string().regex(/^#[0-9a-f]{6}$/i),
surface: z.string().regex(/^#[0-9a-f]{6}$/i),
primary: z.string().regex(/^#[0-9a-f]{6}$/i),
text: z.string().regex(/^#[0-9a-f]{6}$/i),
muted: z.string().regex(/^#[0-9a-f]{6}$/i),
}),
spacing: z.object({
unit: z.number().min(4).max(16),
scale: z.array(z.number()).length(4),
}),
radii: z.object({
sm: z.number(),
md: z.number(),
lg: z.number(),
}),
});
type TokenSet = z.infer<typeof TokenSchema>;
// 2. The compiler
async function compileTheme(
description: string,
apiKey: string
): Promise<TokenSet> {
const client = new OpenAI({ apiKey });
const completion = await client.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content:
"You are a design-token compiler. " +
"Convert the user's theme description into the exact JSON schema provided. " +
"All colors must be valid 6-digit hex. " +
"The spacing unit must be a multiple of 4. " +
"The scale array must have exactly 4 values.",
},
{
role: "user",
content: description,
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "theme_tokens",
strict: true,
schema: zodToJsonSchema(TokenSchema),
},
},
});
const raw = JSON.parse(completion.choices[0].message.content!);
return TokenSchema.parse(raw);
}
// 3. CLI entrypoint
async function main() {
const description = await Bun.file("theme.manifest.txt").text();
const tokens = await compileTheme(description, process.env.OPENAI_API_KEY!);
await Bun.write("tokens.json", JSON.stringify(tokens, null, 2));
console.log("Compiled theme:", tokens.name);
}
main();
Вам понадобятся zod, openai и zod-to-json-schema в качестве зависимостей. Скрипт предполагает использование Bun, но заменить Bun.file и Bun.write на fs.promises тривиально.
Компромиссы, которые нельзя игнорировать
Это работает, но не бесплатно.
Детерминизм. Даже при нулевой температуре и включённых структурированных выходах одно и то же описание может давать немного разные hex-коды в разных версиях модели. Если вы перезапустите компилятор после обновления OpenAI, ваши снапшот-тесты могут упасть. Вы должны закоммитить сгенерированный tokens.json и перекомпилировать только при изменении манифеста.
Латентность. Вызов LLM на этапе сборки добавляет секунды, иногда десятки секунд. Не компилируйте темы при каждом hot reload. Запускайте компилятор как pre-commit hook или в CI при изменении файла манифеста.
Жёсткость схемы. LLM не может изобретать новые категории токенов. Если вашей дизайн-системе позже понадобится elevation.shadows.xl, вы должны обновить схему Zod, системный промпт и компилятор, прежде чем модель сможет это выдать. Это фича, а не баг. Она держит DSL ограниченным.
Доступность. LLM не знает, соответствует ли ваш основной цвет на фоне требованиям контрастности WCAG. Вы должны валидировать сгенерированные токены отдельной проверкой. Добавьте это после вызова TokenSchema.parse:
function contrastRatio(hex1: string, hex2: string): number {
// WCAG relative luminance calculation
const lum = (hex: string) => {
const rgb = parseInt(hex.slice(1), 16);
const [r, g, b] = [(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff];
const f = (c: number) => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
};
const l1 = lum(hex1) + 0.05;
const l2 = lum(hex2) + 0.05;
return l1 > l2 ? l1 / l2 : l2 / l1;
}
// After parsing tokens
const ratio = contrastRatio(tokens.colors.primary, tokens.colors.background);
if (ratio < 4.5) {
throw new Error(`Contrast ratio ${ratio.toFixed(2)} fails WCAG AA`);
}
Когда это ломается: проблема взаимодействия
Этот подход прекрасно работает для цветовых палитр, шкал отступов и семейств шрифтов. Он разваливается, когда токены взаимодействуют способами, которые трудно описать прозой.
Семантические токены — классическая ловушка. Вы можете написать «используйте основной цвет для ссылок и кнопок». Но как насчёт отключённых кнопок? Как насчёт ссылок внутри предупреждающего баннера, который уже использует основной цвет? Это не решения на уровне темы. Это правила на уровне компонентов, и они должны находиться в вашей библиотеке компонентов, а не в файле манифеста.
Ограничивайте манифест примитивными токенами. Пусть компоненты управляют семантикой.
Часто задаваемые вопросы
Что такое DSL с ограниченным контекстом? Небольшой язык с узкой областью применения. В данном случае «языком» является английская проза, описывающая только дизайн-токены, управляемая строгой схемой.
Будет ли одно и то же описание всегда давать одни и те же токены? Не всегда. Выходные данные LLM варьируются в зависимости от версий моделей и провайдеров. Коммитьте сгенерированные токены и используйте снапшот-тесты для обнаружения дрейфа.
Можно ли использовать это для стилей на уровне компонентов? Нет. Манифест должен определять только примитивные токены, такие как цвета и отступы. Семантика компонентов принадлежит вашей библиотеке компонентов.
Нужен ли мне OpenAI, или подойдут другие модели? Подойдёт любая модель, поддерживающая структурированный JSON-вывод и следование системному промпту. Локальные модели вроде Llama 3.3 жизнеспособны, если у вас есть железо, чтобы запускать их достаточно быстро для вашего пайплайна сборки.
Начните с десятистрочного манифеста и строгой схемы
Выберите один ограниченный контекст, например, маркетинговый лендинг или админ-панель. Напишите десятистрочный манифест, определите схему Zod без опциональных полей и сгенерируйте свой первый файл токенов. Закоммитьте вывод. Напишите тест, который перекомпилирует манифест и сравнит его с закоммиченным снапшотом.
Если снапшот дрейфует без изменения манифеста, ваш компилятор недетерминирован. Исправьте системный промпт или зафиксируйте версию модели, пока он не станет детерминированным.
Вашей дизайн-системе не нужен очередной плагин для Figma. Ей нужен единый источник истины, который могут читать и дизайнеры, и разработчики. Простой английский читаем. Схема делает его надёжным.