Да, вы можете описать тему на английском и получить работающую дизайн-систему. Подвох в том, что описание на английском — это не промпт. Это исходный файл. И как любой исходный файл, ему нужен компилятор, система типов и тесты.

Если вы будете относиться к нему как к запросу чат-бота, во вторник вы получите одну цветовую палитру, а в понедельник — другую. Если вы будете относиться к нему как к DSL с ограниченным контекстом и строгой схемой, вы получите воспроизводимые токены, которые сможет потреблять ваша дизайн-система.

Дизайн-токены — это проблема синхронизации, а не творческая задача

Большинство команд не испытывают трудностей с выбором цветов. Они не могут сохранить консистентность цветов между Figma, CSS-переменными и библиотеками компонентов. Типичный рабочий процесс выглядит так: дизайнер обновляет hex-код в гайдлайне, разработчик копирует его в JSON-файл, другой разработчик ссылается на неверный ключ в React-компоненте, и через три месяца в одном месте у вас #1a1a2e, а в другом — #1b1b2f.

Настоящая боль — это передача. Figma — это не код. JSON — это не инструмент дизайна. Английский язык находится посередине. Это единственный формат, который и дизайнеры, и разработчики могут прочитать без обучения.

Вопрос не в том, может ли LLM превратить английский в hex-коды. Вопрос в том, можно ли сделать этот процесс достаточно надёжным, чтобы запускать его в CI.

Как на самом деле работает theme-to-code

Архитектура проста. Вы пишете короткий манифест темы на простом английском. Скрипт передаёт его LLM с ограниченным форматом вывода. LLM возвращает структурированные токены. Вы записываете эти токены на диск, проверяете типы и коммитите их.

Вот пайплайн:

  1. Манифест: Файл .txt или .md, описывающий тему прозой.
  2. Схема: Определение Zod (или JSON Schema), которое должен вернуть LLM.
  3. Компилятор: Скрипт, который читает манифест, вызывает LLM со структурированным выводом и записывает tokens.json.
  4. Ворота: Тест, который падает, если сгенерированные токены отклоняются от снапшота или нарушают ограничения.

Ключ — в третьем шаге. Вы не просите модель «сгенерировать тёмную тему». Вы просите её заполнить схему. Это превращает творческое письмо в задачу извлечения структурированных данных, с которой 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. Ей нужен единый источник истины, который могут читать и дизайнеры, и разработчики. Простой английский читаем. Схема делает его надёжным.