# TASK: Реализация блока «Свободный холст» (custom_canvas) с использованием svelte-grid

## 1. КОНТЕКСТ И ЦЕЛЬ
В SaaS-конструктор micro-landing страниц `libro.az` необходимо добавить новый тип блока `custom_canvas` («Свободный холст»). 
Холст представляет собой 12-колоночную сетку, внутри которой пользователь может свободно перемещать и менять размер дочерних элементов (текст, кнопка, картинка, фигура).

Для интерактивного взаимодействия в режиме редактирования используется библиотека `svelte-grid`.

---

## 2. АРХИТЕКТУРА ДАННЫХ БЛОКА

Блок хранится в JSON-колонке `blocks` таблицы `pages` в общем формате `{ id, type, data, order }`.

### JSON-структура `data` для `custom_canvas`:
```json
{
  "rowHeight": 36,
  "cols": 12,
  "elements": [
    {
      "id": "el_1",
      "type": "text",
      "x": 0, "y": 0, "w": 12, "h": 2,
      "data": { "text": "Свободный заголовок", "fontSize": 20, "align": "center", "color": "#111827" }
    },
    {
      "id": "el_2",
      "type": "button",
      "x": 2, "y": 2, "w": 8, "h": 2,
      "data": { "text": "Забронировать", "url": "[https://wa.me/](https://wa.me/)...", "bgColor": "#00A69C", "textColor": "#ffffff" }
    }
  ],
  "_bgColor": "#ffffff",
  "_pt": 16,
  "_pb": 16
}
Обратите внимание: В svelte-grid координаты x и y начинаются с 0 (индексация от 0 до 11).

3. ПОШАГОВЫЙ ПЛАН РЕАЛИЗАЦИИ
Шаг 1. Бэкенд Валидация (UpdateBlocksRequest.php)
Добавить custom_canvas в правило валидации типов:

PHP
'blocks.*.type' => 'required|in:avatar,heading,text,button_link,social_links,image,video,faq,spacer,divider,custom_canvas',
Шаг 2. Инициализация в Editor.svelte
Добавить дефолтные данные в defaults['custom_canvas']:

JavaScript
defaults['custom_canvas'] = {
  rowHeight: 36,
  cols: 12,
  elements: [
    {
      id: crypto.randomUUID(),
      type: 'text',
      x: 0,
      y: 0,
      w: 12,
      h: 2,
      data: { text: "Новый заголовок холста", fontSize: 18, align: "center", color: "#111827" }
    }
  ]
};
Форматирование данных под svelte-grid:
svelte-grid требует массив объектов, где координатная сетка лежит в ключе [cols], например:

JavaScript
// Преобразование элементов libro.az в формат svelte-grid:
function mapToGridItems(elements) {
  return elements.map(el => ({
    ...el,
    5: { x: el.x, y: el.y, w: el.w, h: el.h }, // Координаты для 12 колонок (или дефолтной сетки)
    [12]: { x: el.x, y: el.y, w: el.w, h: el.h }
  }));
}
Шаг 3. Компонент модалки редактирования (CanvasEditor.svelte)
Создать компонент управления холстом внутри модального окна редактирования.

Обязательная логика:
Использовать svelte-grid в интерактивном режиме (fastStart: true, scroller: true).

При событии изменения позиции/размера (события change / adjust в svelte-grid) обновлять координаты элементов в editBlockData.elements.

Сразу вызывать функцию liveUpdate(), чтобы изменения отображались в реальном времени в предпросмотре телефона.

Предусмотреть панель инструментов для добавления элементов (+ Текст, + Кнопка, + Картинка, + Фигура) и кнопку удаления активного элемента.

Пример структуры CanvasEditor.svelte:
Svelte
<script>
  import Grid from 'svelte-grid';
  import gridHelp from 'svelte-grid/build/helper/index.js';

  let { editBlockData, onUpdate } = $props(); // Svelte 5 Runes

  const COLS = 12;

  // Формируем сетку для svelte-grid
  let items = $state(
    editBlockData.elements.map(el => ({
      id: el.id,
      type: el.type,
      data: el.data,
      [COLS]: { x: el.x, y: el.y, w: el.w, h: el.h }
    }))
  );

  function onChange(event) {
    // Получаем обновленные элементы с новыми x, y, w, h
    const updatedGridItems = event.detail.items;

    editBlockData.elements = updatedGridItems.map(item => ({
      id: item.id,
      type: item.type,
      data: item.data,
      x: item[COLS].x,
      y: item[COLS].y,
      w: item[COLS].w,
      h: item[COLS].h
    }));

    // Запуск liveUpdate() в родительском компоненте
    onUpdate();
  }

  function addElement(type) {
    const newEl = {
      id: crypto.randomUUID(),
      type: type,
      x: 0,
      y: gridHelp.findSpace(items, COLS).y || 0,
      w: 6,
      h: 2,
      data: getDefaultDataForType(type)
    };

    items = [...items, { ...newEl, [COLS]: { x: newEl.x, y: newEl.y, w: newEl.w, h: newEl.h } }];
    editBlockData.elements.push(newEl);
    onUpdate();
  }
</script>

<div class="space-y-4">
  <!-- Панель инструментов добавления элементов -->
  <div class="flex gap-2 border-b pb-3">
    <button type="button" onclick={() => addElement('text')} class="btn-sm">+ Текст</button>
    <button type="button" onclick={() => addElement('button')} class="btn-sm">+ Кнопка</button>
    <button type="button" onclick={() => addElement('image')} class="btn-sm">+ Картинка</button>
  </div>

  <!-- Интерактивный холст -->
  <div class="border rounded-lg bg-gray-50 p-2 min-h-[300px]">
    <Grid 36} COLS]]} cols="{[[12," let:item on:change="{onChange}" rowHeight="{editBlockData.rowHeight" {items} ||>
      <div class="h-full w-full border-2 border-dashed border-blue-400 bg-white/90 rounded p-2 relative group flex items-center justify-center">
        <span class="text-xs font-medium truncate">{item.type}: {item.data.text || ''}</span>
      </div>
    </Grid>
  </div>
</div>
Шаг 4. Публичный рендеринг (BlockRenderer.svelte)
На публичной странице /{slug} и внутри макета телефона (Live Preview) НЕ нужно инициализировать тяжелую JS-библиотеку svelte-grid.
Отображение выполняется через легкий нативный CSS Grid (12 колонок), используя полученные x, y, w, h:

Svelte
{#if block.type === 'custom_canvas'}
  <div 
    class="w-full grid grid-cols-12 gap-1 relative"
    style="
      grid-auto-rows: {block.data.rowHeight || 36}px;
      background-color: {block.data._bgColor || 'transparent'};
      padding-top: {block.data._pt || 0}px;
      padding-bottom: {block.data._pb || 0}px;
    "
  >
    {#each block.data.elements as item (item.id)}
      <div
        class="relative flex items-center justify-center overflow-hidden"
        style="
          grid-column: {item.x + 1} / span {item.w};
          grid-row: {item.y + 1} / span {item.h};
        "
      >
        {#if item.type === 'text'}
          <p style="color: {item.data.color}; font-size: {item.data.fontSize}px; text-align: {item.data.align};">
            {item.data.text}
          </p>
        {:else if item.type === 'button'}
          <a 
            href={item.data.url}
            class="w-full h-full flex items-center justify-center rounded-lg font-semibold transition-transform active:scale-95"
            style="background-color: {item.data.bgColor}; color: {item.data.textColor};"
          >
            {item.data.text}
          </a>
        {:else if item.type === 'image'}
          <img src={item.data.url} alt="" class="w-full h-full object-cover rounded-lg" />
        {/if}
      </div>
    {/each}
  </div>
{/if}
Важно для CSS Grid: Так как svelte-grid использует 0-index (x: 0), а CSS Grid начинается с 1, в grid-column и grid-row прибавляем + 1 (item.x + 1).

4. ИТОГОВЫЕ ТРЕБОВАНИЯ К КОДУ
Написать код строго по синтаксису Svelte 5 ($state, $props, $derived).

Сохранять совместимость с существующей цепочкой editBlockData -> liveUpdate() -> save().

Мобильная адаптивность: CSS Grid на публичной странице должен быть с w-full, чтобы сетка пропорционально сжималась под любой экран смартфона.