5. Как сделать тему

⚠️ Целевая модель. Сейчас витрина — SSR-Razor с CSS-темами (19 шт., меняют вид, но не компоновку). Этот раздел описывает, как темы должны работать — Liquid-бандлы в одном рантайме. По нему реализуем рантайм тем.

Тема = бандл файлов (шаблоны + стили + скрипты + настройки), который рантайм витрины исполняет на объектной модели. Тема владеет видом витринных экранов; корзину/оплату/запись/подсчёты держит платформа.

Структура темы

theme/
  theme.json            # манифест: имя, вертикаль, версия, вход
  templates/            # страницы (Liquid)
    home.liquid
    category.liquid
    product.liquid
    cart.liquid
    booking.liquid
  sections/             # переиспользуемые блоки
    header.liquid  hero.liquid  product-card.liquid  footer.liquid
  assets/
    theme.css   theme.js   # свои стили/скрипты (без внешних CDN)
  settings.json         # схема настроек, редактируемых владельцем
  checkout.json         # темизация checkout: токены + разрешённые блоки

Наследование: тема переопределяет только нужные файлы; отсутствующие берутся из базовой темы. «Лёгкая» тема = home.liquid + product-card.liquid + theme.css.

Шаблоны и объекты

Каждый templates/*.liquid получает контекст своей страницы (таблица контекста). Пример карточки:

{% for p in products %}
  <a class="card" href="/p/{{ p.slug | default: p.id }}">
    <img src="{{ p.imageUrl }}" alt="{{ p.name }}">
    <div class="card__name">{{ p.name }}</div>
    <div class="card__price">
      {{ p.effectivePrice }} {{ shop.currency }}
      {% if p.oldPrice %}<s>{{ p.oldPrice }}</s>{% endif %}
    </div>
    {% if p.type == 'product' and shop.flags.hasOrders %}
      <button data-sf-add="{{ p.id }}">В корзину</button>
    {% endif %}
  </a>
{% endfor %}

Liquid — песочница: нет доступа к файлам, БД, сети, C#. Только объекты контекста + фильтры (форматирование, не вычисление денег).

Поведение: JS-API рантайма

Динамику (корзина, варианты, слоты) тема делает через готовый JS рантайма — не реализует логику сама. Два способа:

1. Data-атрибуты (декларативно, без кода):

<button data-sf-add="{{ p.id }}">В корзину</button>
<button data-sf-qty="+1">+</button>
<span data-sf-cart-count></span>

Рантайм сам вешает обработчики, обновляет корзину, перерисовывает счётчик.

2. JS-API (императивно):

sf.cart.add(itemId, qty)      // → Promise<cart>
sf.cart.setQty(itemId, qty)
sf.cart.get()
sf.product.selectVariant(options)   // {Цвет:'Красный',Размер:'M'} → variant
sf.booking.availability(serviceId, staffId, date)
sf.booking.hold(slot)

Все мутации идут в API рантайма (same-origin) → ядро добавляет ключ. Итоги возвращает сервер; тема их только показывает.

Checkout — токены + блоки, не разметка

Checkout платформенный (безопасность/PCI). Тема влияет через checkout.json:

Настройки темы (для владельца)

settings.json описывает, что владелец меняет в панели без кода:

[
  {"key":"accent","type":"color","label":"Акцент","default":"#4F46E5"},
  {"key":"grid","type":"select","label":"Сетка","options":["cozy","standard","compact"]},
  {"key":"heroImage","type":"image","label":"Заглавная картинка"}
]

Значения приходят в Liquid как settings.accent и т.п.

Ограничения (песочница)

Тема не может: выполнять серверный код; ходить в ядро напрямую или к сторонним хостам с сервера; считать/менять цены, итоги, остатки; трогать разметку/логику оплаты; читать данные чужих магазинов. Всё это — платформа.

Чек-лист «моя тема готова»