{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "/block-schema.json",
  "title": "PageCraft Block Field Schema",
  "description": "Meta-schema that validates the `schema` field of a PageCraft block.\nA field schema describes the editable data structure of the block.\nThis data is filled in by the editor and passed to the Liquid template as variables.\n\nBLOCK TYPES:\n- null / 'schema' — Liquid template with optional user-defined fields. Variables from `fields` are available in the template directly: {{ props.field_key }}.\n- 'markdown' — Liquid template + WYSIWYG rich text editor. The markdown content is stored in a reserved system field `_markdown` inside `placeholder_values` (NOT defined in `fields`). At render time `_markdown` is converted to HTML and injected as `{{ content }}` into the Liquid template. A markdown block may also define extra `fields` (e.g. color, size) which are passed to the template alongside `content`.",
  "type": "object",
  "x-maxSlotDepth": 1,
  "x-maxSlotDepthDescription": "Максимальный уровень вложенности блоков в слоты композиции: 0 — блок верхнего уровня, 1 — блок внутри слота. Единственный источник числа: его читают и бэкенд (App\\Service\\SlotLimits), и редактор (front/src/lib/slots.ts).",
  "required": ["type", "fields"],
  "additionalProperties": false,
  "properties": {
    "type": {
      "type": "string",
      "const": "object",
      "description": "Root type is always 'object'"
    },
    "label": {
      "type": "string",
      "description": "Human-readable title shown in the editor form"
    },
    "fields": {
      "type": "object",
      "description": "Map of field key → field definition. Keys become Liquid variables in the template. The names size/first/last are forbidden — they are reserved Liquid accessors (length/first/last item): {{ props.size }} with no value returns a number, not the default, so | default won't apply. Use box_size, first_item, etc.",
      "propertyNames": { "not": { "enum": ["size", "first", "last"] } },
      "additionalProperties": { "$ref": "#/$defs/fieldDef" }
    },
    "examples": {
      "type": "array",
      "description": "Optional list of preset data examples for this block. Examples appear as a selector in the block editor and block insertion form, letting editors quickly fill the form with sample data.",
      "items": {
        "type": "object",
        "required": ["title", "data"],
        "additionalProperties": false,
        "properties": {
          "title": {
            "type": "string",
            "description": "Short display name for the example shown in the selector dropdown",
            "examples": ["Пример с картинкой"]
          },
          "description": {
            "type": "string",
            "description": "Optional longer description of the example (tooltip or hint text)"
          },
          "data": {
            "type": "object",
            "description": "Field values to populate the form. Keys must match top-level field names defined in 'fields'. Unknown keys are rejected by validation.",
            "examples": [{ "title": "Заголовок примера", "subtitle": "Подзаголовок" }]
          }
        }
      }
    },
    "bindingSlots": {
      "type": "object",
      "description": "SSR-биндинги блока. Наличие поля делает блок SSR-блоком.",
      "additionalProperties": {
        "type": "object",
        "required": ["resultType", "allowedSources"],
        "additionalProperties": false,
        "properties": {
          "resultType": {
            "type": "string",
            "description": "Тип результата. Все sources в allowedSources должны иметь этот resultType."
          },
          "default": {
            "type": "string",
            "description": "ID source по умолчанию. Должен входить в allowedSources."
          },
          "allowedSources": {
            "type": "array",
            "items": { "type": "string" },
            "minItems": 1,
            "description": "Список допустимых источников данных для этого слота."
          }
        }
      }
    },
    "slots": {
      "type": "object",
      "description": "Слоты композиции: области блока, в которые редактор вкладывает другие блоки. Наличие непустого поля делает блок контейнером. Не путать с bindingSlots: те подставляют в шаблон данные из внешнего источника ({{ data.<имя> }}), эти — собранный HTML вложенных блоков ({{ slots.<имя> }}). Имена слотов не должны пересекаться с именами bindingSlots. Вложенность ограничена одним уровнем: блок, лежащий в слоте, своих детей иметь не может.",
      "propertyNames": { "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
      "additionalProperties": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "label": {
            "type": "string",
            "description": "Название слота в редакторе. Без него показывается имя ключа.",
            "examples": ["Левая колонка"]
          },
          "max": {
            "type": "integer",
            "minimum": 1,
            "description": "Максимум блоков в слоте. Без ограничения, если не задан."
          }
        }
      },
      "examples": [{ "left": { "label": "Левая колонка" }, "right": { "label": "Правая колонка", "max": 1 } }]
    },
    "client": {
      "type": "object",
      "description": "Клиентское поведение блока во время рендера/SSR. Только для блоков с bindingSlots.",
      "additionalProperties": false,
      "properties": {
        "refreshOn": {
          "type": "array",
          "uniqueItems": true,
          "description": "События клиентского рантайма, при получении которых блок перерисовывается на сервере и подменяется в DOM. Указывайте только реально нужные события — каждая подписка увеличивает количество SSR-запросов.",
          "items": {
            "oneOf": [
              {
                "const": "cart.change",
                "title": "cart.change — изменение корзины",
                "description": "Состав или количество товаров в корзине изменилось (добавление, удаление, смена количества). Подходит для блоков, чьё содержимое зависит от корзины: сводки итогов, виджеты «в корзине», рекомендации по содержимому."
              }
            ]
          }
        }
      }
    }
  },
  "$defs": {
    "fieldDef": {
      "description": "Definition of a single editable field",
      "oneOf": [
        {
          "title": "Text / Textarea / URL / Email field",
          "type": "object",
          "required": ["type"],
          "additionalProperties": false,
          "properties": {
            "type": {
              "type": "string",
              "enum": ["text", "textarea", "url", "email"],
              "description": "Input type: text=single line, textarea=multiline, url/email=validated input"
            },
            "label": { "type": "string", "description": "Field label in the editor" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": { "type": "string", "description": "Default value pre-filled in the form" },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." }
          }
        },
        {
          "title": "Image upload field",
          "type": "object",
          "required": ["type"],
          "additionalProperties": false,
          "properties": {
            "type": {
              "type": "string",
              "const": "image",
              "description": "Image field. Renders as an upload button in the editor. Requires S3 to be configured in user settings. Stores the public URL of the uploaded image. Use in Liquid template: {{ props.field_key }}"
            },
            "label": { "type": "string", "description": "Field label in the editor" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": { "type": "string", "description": "Default image URL (optional)" },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." }
          }
        },
        {
          "title": "Icon picker field",
          "type": "object",
          "required": ["type"],
          "additionalProperties": false,
          "properties": {
            "type": {
              "type": "string",
              "const": "icon",
              "description": "Icon picker field. Opens an Iconify search modal in the editor. The stored value is a raw SVG string (not an icon id). Use in Liquid template: {{ props.field_key }} — outputs inline SVG HTML."
            },
            "label": { "type": "string", "description": "Field label in the editor" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": {
              "type": "string",
              "description": "Optional default Iconify icon id (e.g. 'mdi:home'). The id is resolved to SVG at form initialization."
            },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." }
          }
        },
        {
          "title": "Select (dropdown) field",
          "type": "object",
          "required": ["type", "options"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "select" },
            "label": { "type": "string" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": { "type": "string", "description": "Default selected value (must match one of options[].value)" },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." },
            "options": {
              "type": "array",
              "description": "Available choices",
              "minItems": 1,
              "items": {
                "type": "object",
                "required": ["label", "value"],
                "additionalProperties": false,
                "properties": {
                  "label": { "type": "string", "description": "Displayed text" },
                  "value": { "type": "string", "description": "Stored value used in the template" }
                }
              }
            }
          }
        },
        {
          "title": "Radio button group field",
          "type": "object",
          "required": ["type", "options"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "radio" },
            "label": { "type": "string" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": { "type": "string", "description": "Default selected value (must match one of options[].value)" },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." },
            "options": {
              "type": "array",
              "description": "Available choices",
              "minItems": 1,
              "items": {
                "type": "object",
                "required": ["label", "value"],
                "additionalProperties": false,
                "properties": {
                  "label": { "type": "string", "description": "Displayed text" },
                  "value": { "type": "string", "description": "Stored value used in the template" }
                }
              }
            }
          }
        },
        {
          "title": "Switch (boolean toggle) field",
          "type": "object",
          "required": ["type"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "switch" },
            "label": { "type": "string" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": { "type": "boolean", "description": "Default boolean value (true/false)" },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." }
          }
        },
        {
          "title": "Color picker field",
          "type": "object",
          "required": ["type"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "color", "description": "Color picker field. Stores a hex color string (e.g. '#ff0000'). Use in Liquid template: {{ props.field_key }}" },
            "label": { "type": "string" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": { "type": "string", "description": "Default hex color value (e.g. '#ffffff')" },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." }
          }
        },
        {
          "title": "Date field",
          "type": "object",
          "required": ["type"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "date", "description": "Date picker field. Stores a date string in YYYY-MM-DD format (e.g. '2026-04-06'). Use in Liquid template: {{ props.field_key }}" },
            "label": { "type": "string", "description": "Field label in the editor" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": { "type": "string", "description": "Default date in YYYY-MM-DD format (e.g. '2026-01-01')" },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." }
          }
        },
        {
          "title": "Time field",
          "type": "object",
          "required": ["type"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "time", "description": "Time picker field. Stores a time string in HH:MM format (e.g. '14:30'). Use in Liquid template: {{ props.field_key }}" },
            "label": { "type": "string", "description": "Field label in the editor" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": { "type": "string", "description": "Default time in HH:MM format (e.g. '09:00')" },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." }
          }
        },
        {
          "title": "Datetime field",
          "type": "object",
          "required": ["type"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "datetime", "description": "Date and time picker field. Stores a datetime string in YYYY-MM-DDTHH:MM format (e.g. '2026-04-06T14:30'). Use in Liquid template: {{ props.field_key }}" },
            "label": { "type": "string", "description": "Field label in the editor" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "required": {
              "type": "boolean",
              "description": "Поле обязательно для заполнения. Если true — поле должно иметь default значение."
            },
            "default": { "type": "string", "description": "Default datetime in YYYY-MM-DDTHH:MM format (e.g. '2026-01-01T09:00')" },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." }
          }
        },
        {
          "title": "Object (nested fields) field",
          "type": "object",
          "required": ["type", "fields"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "object" },
            "label": { "type": "string" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." },
            "fields": {
              "type": "object",
              "description": "Nested field definitions. Access in template: {{ props.parent.child }}. The names size/first/last are forbidden — reserved Liquid accessors (see root 'fields'). Use box_size, first_item, etc.",
              "propertyNames": { "not": { "enum": ["size", "first", "last"] } },
              "additionalProperties": { "$ref": "#/$defs/fieldDef" }
            }
          }
        },
        {
          "title": "Array (repeatable items) field",
          "type": "object",
          "required": ["type", "item"],
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "const": "array" },
            "label": { "type": "string" },
            "description": {
              "type": "string",
              "description": "Пояснение к полю, отображается под контролом в редакторе. Добавлять только если label недостаточно информативен."
            },
            "tab": { "type": "string", "description": "Optional tab name to group this field under in the editor form. Fields sharing the same tab value are grouped together; fields without a tab go to 'Общие'." },
            "min": { "type": "integer", "minimum": 0, "description": "Minimum number of items (also sets initial count)" },
            "max": { "type": "integer", "minimum": 1, "description": "Maximum number of items" },
            "itemLabel": {
              "type": "string",
              "description": "Template string for item label in editor, e.g. '{{title}}'. Supports {{fieldKey}} substitution."
            },
            "item": {
              "$ref": "#/$defs/fieldDef",
              "description": "Definition of each array item. Must be type 'object' with nested fields. Access in template via {% for item in props.items %}."
            }
          }
        }
      ]
    }
  }
}
