Привет, Хабр!

Я принёс Specify — open‑source генератор веб‑приложений из спецификации. Это как Framer, только без визуального редактора и без чёрной магии AI под капотом.

Из коробки Specify умеет генерировать PWA (SPA) на Svelte, с бронебойной автономностью, приложение откроется в лифте, на парковке, при белых списках и даже если физически не станет сервера. Есть интеграция с open‑source бэкенд‑платформой Appwrite, о которой я расскажу ниже.

Зачем я это сделал

Вообще, я не разработчик, мой основной опыт в продуктовом дизайне, а программирую я для души).

Примерно 3 года назад у меня возникла мотивация закрыть весь цикл разработки продукта самостоятельно. Если 15–20 лет назад я мог сам создать сайт, сверстать дизайн и натянуть HTML c CSS на CMS, то сегодня этого опыта недостаточно. Чтобы сделать современное веб‑приложение, нужно погрузиться во фронтенд‑фреймворки, сборщики проектов, современный бэк с веб‑сокетами и так далее

Я быстро понял, что ручная верстка и перенос изменений из дизайна в код сильно меня тормозят. И я начал делать, по сути, свой аналог Framer. За 3 года я прошел путь от автоматизации переноса дизайна в код (парсинга JSON‑данных дизайна и полной сборки приложения) — до понимания, что нужно делать свой редактор.

Я хотел сделать свой полноценный визуальный редактор, но пока что на это нет ресурса. Зато я понял, что можно сделать MVP без собственного редактора, используя максимум возможностей IDE для улучшения DX.

Так появился Specify, объединяющий абстракцию дизайна и фронтенда в одной спецификации.

Что такое Specify

Specify — это не фреймворк и не билдер, хотя похоже на одно и другое. Это декларативная спецификация и компилятор (транслятор) спецификации в код приложения.

Я делал его для решения своих задач, но если это поможет другим дизайнерам и командам перестать играть в сломанный телефон между дизайном и разработкой — буду рад.

Дизайнер, если ты ещё здесь, Specify — это не больно:

  1. Спецификация на языке дизайна — здесь почти вся терминология из Figma.

  2. IDE помогает писать: автоподстановка свойств и значений.

  3. Любое изменение мгновенно отображается в браузере (HMR).

  4. Не хочешь писать спецификацию руками — можно попробовать генерировать её с помощью AI и получить что‑то похожее на Google Stitch (в проекте для этого есть Skill).

Лучше один раз увидеть, чем сто раз услышать:

(Слева YAML‑экрана, под ним Svelte‑код этого экрана, который обновляется в реальном времени, справа вкладка браузера.)

Чтобы дизайнерам дальше было проще воспринимать Specify, проведу параллели с Figma.

Стили

В Specify в папке specs/ находится файл tokens.yaml — почти как Figma Variables. В specs/components/ — YAML‑файлы компонентов, в specs/screens/ — экранов. У каждого экрана и компонента есть слои (frame, text, image и так далее), у каждого слоя — стили.

Стили сгруппированы по префиксам и почти дублируют свойства в Figma:

  • pos — секция Position

  • lay — секция Auto Layout

  • vis — секция Appearance

  • txt — секция Typography

  • fill, stroke, shadow, blur — самостоятельные, без префиксов.

Пример:

styles:
  lay: column                 # Auto Layout — Flow: Vertical
  lay-width: fill-container   # Тянем на всю ширину
  lay-align-h: center         # Выравнивание по горизонтали
  lay-align-v: center         # Выравнивание по вертикали

В Figma для состояний делают варианты компонента. В Specify состояния — это стили, почти как в CSS:

  • hover — наведение курсора

  • focus — фокус в поле или при навигации с клавиатуры

  • pressed — клик/нажатие

  • disabled — неактивный элемент

  • selected — выбранный (поддерживает вложенные: hover, focus, pressed, disabled)

  • valid — поля внутри контейнера прошли валидацию (с вложенными состояниями)

  • invalid — не прошли валидацию (тоже с вложенными).

Пример:

styles:
  fill: "#F5F5F5"
  hover:
    fill: "#EBEBEB"
    pressed:
	  fill: "#E0E0E0"
    focus:
	  stroke:
        - color: white
          weight: 1
          position: inside
        - color: blue
          weight: 2
          position: outside
    invalid:
	  stroke:
        - color: red
          weight: 2
          position: outside

Свойства компонентов и реактивность

У компонентов есть:

  • props — входные данные снаружи (как в Figma).

  • state — внутреннее состояние.

  • stores — глобальные реактивные переменные с логикой, доступны из любого компонента.

  • logic — пользовательский код, подключаемый к компоненту.

  • data — статичные или динамические данные с бэкенда.

У экранов так же, только вместо props — path и params (о них в документации).

Пример:

button:
  props:         
    label: Text                       # Объявляем props
	
  layers:
    - text:
        content: "{{ props.label }}"  # Выводим значение props

Вызываем компонент на экране и пробрасываем props:

demo:
  path: /demo

  layers:
    - c.button:
        props:
		  label: Нажми на меня

Условия и циклы

cond-if позволяет показывать элементы по условию.

demo:
  path: /demo

  state:
    count: 0
    min: -10
    max: 10
	
  layers:
    - text:
        content: "Счетчик лайков: {{ state.count }}"
    - c.button:
        # Кнопка отображается, если state.count больше state.min
        cond-if: "{{ state.count > state.min }}"
        props:
          label: 👎
        on-click:                                     
          set-state:
            count: "{{ state.count - 1 }}"
    - c.button:
        # Кнопка отображается, если state.count меньше state.max
        cond-if: "{{ state.count < state.max }}"
        props:
          label: 👍
        on-click:                                     
          set-state:
            count: "{{ state.count + 1 }}"

Циклы размножают слой по массиву данных:

demo:
  path: /demo

  state:
    isAnnually: false
  data:
    pricing:                        # Массив данных
      - id: 1
        title: Free
        monthly: 0
        annually: 0
      - id: 2
        title: Pro
        monthly: 100
        annually: 80
      - id: 3
        title: Max
        monthly: 200
        annually: 150
	
  layers:
    - frame:
        for: item in data.pricing  # Создаём цикл, этот frame будет повторяться столько раз, сколько записей в массиве
        key: item.id               # Уникальный ключ для корректного диффа в DOM
        layers:
          - text:
            content: "{{ item.title }}" # Выводим название
          - text:
              content: "Цена: {{ state.isAnnually ? item.annually : item.monthly }} ₽" # Выводим цену по условию state.isAnnually

Триггеры событий

Аналог Interactions из Figma. События:

  • on-click — клик.

  • on-mount / on-unmount — появление/исчезновение слоя.

  • on-change — изменение значения (для слоёв field и text).

  • on-submit — отправка формы.

  • on-keydown — нажатие клавиши.

События могут запустить действия:

  • set-state — изменить state.

  • set-props — для исключительных случаев с редактируемыми props (подробнее в документации).

  • navigate-to — переход на другой экран (с анимацией) или внешнюю ссылку.

  • navigate-back — назад.

  • set-notification — снекбар.

  • post-data — отправка в Appwrite.

  • run — выполнение пользовательского кода, от console.log() до вызова своей функции или стора.

  • submit / reset — отправить/сбросить форму.

Пользовательский код

Не все компоненты реально сделать без дополнительной логики. Например, в компоненте slider-md есть функция createSlider, которая следит за захватом ползунка и обновляет value.

Specify даёт возможность подключить свой код:

screenName:
  path: /demo

  logic:
    - fnName  # Функция из src/logic

  stores:
    - cart    # Стор корзины из src/stores

  on-keydown:
    - key: Control+S
      run: "{{ logic.fnName(stores.cart.list) }}" # Вызовем функцию при нажатии Control+S
  
  layers:
    - text:
        content: "В корзине: {{ stores.cart.list.length }}" # Выведем количество товаров
    - c.button:
        props:
          label: Добавить
            on-click:
              run: "{{ stores.cart.add({ name: 'Эспрессо', price: 150 }) }}" # Добавляем в корзину товар

Расширяйте приложение, не затрагивая сгенерированный код.

Интеграция с Appwrite

Немного про Appwrite

Это open‑source бэкенд, разворачивается на своём VPS, не сильно сложнее установки WordPress. Юзерфрендли, закрывает много вопросов через веб‑интерфейс, сам выпускает SSL для привязанных доменов. Очень быстро развивается, в этом году переработали API, добавили MongoDB и PostgreSQL. Тянет большие нагрузки, есть публичный бенчмарк, где один сервер держит больше миллиона одновременных WebSocket‑соединений. По экономике: VPS дороже хостинга, но на одном сервере можно вести несколько проектов и хостить сайты.

Запрос данных (get‑data)

Надстройка над Appwrite SDK. Управляет кешем, мгновенно отвечает данными из кеша и затем возвращает актуальные данные. Не кеширует ответы по отдельности, а работает как локальная БД — сама фильтрует и сортирует. Это позволяет приложению предсказуемо работать в оффлайне. Через get-data можно получить данные из базы, профиля пользователя, команд и файлов. Поддерживается Realtime (обновление по WebSocket, включается флагом realtime) и бесконечный скролл (флаг infinite-scroll). Подробнее в документации.

Отправка данных (post‑data)

Тоже надстройка над SDK. В оффлайне складывает запросы в очередь, а как появится сеть — отправляет в Appwrite. Создаёт, обновляет, удаляет записи в БД. Всё, что касается аккаунта пользователя. Управление командами. Загрузка файлов в Storage. Подробнее в документации.

Расширяемость

Specify не навязывает стек. Трансляция спецификации в итоговый код идёт через Vite‑плагины, и вы можете:

  • Написать свой Vite‑плагин и транслировать спецификацию в любой фреймворк: React, Vue, Svelte, Solid — что угодно.

  • Расширить DSL под свой бэкенд: если у вас не Appwrite, а Supabase, Firebase, свой REST или GraphQL — можете дописать свои операторы get-data / post-data и работать с ними как с родными.

Дальнейшие планы

  1. Добавить необходимый минимум базовых компонентов.

  2. Добавить Push‑уведомления, подписка/отписка на клиенте.

  3. Расширить документацию и добавить больше примеров.

P. S. Если тебе откликается Specify и ты хочешь принять участие в развитии (код, документация, примеры, дизайн компонентов) — пиши на kov@lenko.email. Также буду рад фидбеку и звёздочкам на GitHub.