К содержанию
Общее · Разработчикам

Компоненты интерфейса SDK

Показатели, графики, таблицы с переключателями, выбор товаров, редактор, настройки, мастера и сообщения.

Содержание

Плагин описывает, что показать: разделы, показатели, графики, списки, формы, кнопки и действия. Как это выглядит, решает Lovsel. Каждая функция ui.* возвращает обычный словарь, а кабинет отрисовывает его теми же компонентами, что и встроенные разделы: плитки показателей с линией динамики, графики как в «Аналитике», таблицы с переключателями в строках, выбор товаров из каталога, редактор в панели справа, настройки с автосохранением и пошаговые мастера. Поэтому любой плагин выглядит частью Lovsel — в чёрной теме с зелёными акцентами и в светлой теме, на компьютере и на телефоне. HTML, CSS и JavaScript писать не нужно.

Структура файла, манифест, события и публикация описаны в руководстве «Как создать плагин», а полный плагин с нуля — в уроке «Создаём плагин как KOSell: связки товаров». API площадок — в разделах «Плагины для GGsel», «Плагины для FunPay» и «Плагины для Playerok».

python
from lovsel_sdk import WebPlugin, ui, ValidationError

Как выглядит плагин

Все плагины открываются в одной и той же оболочке:

  • Шапка — значок плагина, название, состояние («Работает», «Выключен», «Ошибка», «Ждёт подключения») с цветной точкой, площадка плагина, версия, автор, число разрешений и переключатель «Включён».
  • Вкладки — под шапкой одной строкой: страницы плагина слева, служебные «Настройки», «Журнал» и «О плагине» — справа. Второй боковой панели нет, и в левое меню кабинета разделы плагина не попадают: там только сами плагины. На телефоне строка вкладок прокручивается, активная вкладка всегда видна.
  • Содержимое — карточки с тонкой рамкой; показатели, графики и таблицы — те же, что на главной и в аналитике. Переход между вкладками и шагами мастера — короткая плавная анимация (200–300 мс); если в системе включено «уменьшить движение», анимаций нет.

Плагину ничего для этого делать не нужно: достаточно объявить страницы и вернуть блоки. Цвета, отступы, шрифты, тёмная и светлая тема — забота Lovsel.

Цвет площадки

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

ПлагинЦвет
"marketplace": "ggsel"зелёный GGsel
"marketplace": "funpay"голубой FunPay
"marketplace": "playerok"синий Playerok

Цвет в плагине не задаётся: у блоков есть только смысловые оттенки tone — success, warning, danger, info, neutral и accent (цвет площадки). У линий графика — ещё цвета площадок ggsel, funpay, playerok (см. «Графики»).

Каждая страница — функция, которая возвращает список блоков. Страницы показываются вкладками под шапкой плагина. Порядок объявления — порядок вкладок. Второй боковой панели нет, а в левом меню кабинета — только сами плагины (закреплённые пользователем), без их разделов.

python
app = WebPlugin(MANIFEST)


@app.page("overview", "Обзор", icon="layout-dashboard")
def overview(ctx, params): ...


@app.page("rules", "Правила", icon="list-checks", badge=lambda ctx: len(rules(ctx)) or None)
def rules_page(ctx, params): ...


@app.page("rule", "Правило", hidden=True, parent="rules")   # карточка: вкладки у неё нет
def rule_page(ctx, params): ...


@app.page("help", "Инструкция", icon="book", bottom=True)
def help_page(ctx, params): ...
ПараметрЧто делает
iconИконка вкладки: имя (список — в разделе «Иконки») или эмодзи.
descriptionКороткое пояснение под вкладками: что на этой странице.
groupГруппа вкладок: между группами в строке вкладок — тонкий разделитель.
badgeСчётчик на вкладке: функция badge(ctx) → число или строка. {"value": 3, "tone": "danger"} — другой цвет (danger, warning, success, info, neutral, по умолчанию accent). None, 0 или "" — без счётчика.
parentИдентификатор раздела (app.section), в котором стоит вкладка, или страницы — для скрытой карточки.
hidden=TrueВкладки нет: карточка записи, мастер создания. Открывается кнопкой ui.button(page=..., params=...). Если у скрытой страницы есть parent, подсвечивается вкладка родителя.
bottom=TrueВкладка в конце своей строки («Инструкция»).
marketplaceЦвет площадки для страницы. Обычно не нужен: страницы плагина и так в цвете его площадки.

Функция счётчика вызывается при каждом открытии плагина, поэтому она должна быть быстрой: читайте готовое число из ctx.storage, а пересчитывайте его в обработчиках событий или в фоне. Если функция упала или думала дольше двух секунд, счётчика просто нет.

Разделы и вложенность

app.section(id, title, ...) — вкладка без своей страницы, папка для страниц с parent=id. Нажатие на раздел открывает его первую страницу, а страницы раздела появляются второй строкой вкладок. Разделы вкладываются друг в друга через parent — у каждого уровня своя строка; Lovsel показывает до трёх уровней (более глубокие пункты поднимаются на верхний уровень). Параметры раздела — icon, description, badge, group, bottom, parent, как у страницы. Параметр view ("menu" или "tabs") оставлен для совместимости и на вид не влияет.

python
app = WebPlugin(MANIFEST)

@app.page("overview", "Обзор", icon="layout-dashboard")
def overview(ctx, params): ...

# Раздел: вкладка «Заказы», внутри — вторая строка «Новые · Выполненные»
app.section("orders", "Заказы", icon="order",
            badge=lambda ctx: ctx.storage.get("waiting", 0) or None)

@app.page("orders_new", "Новые", parent="orders",
          badge=lambda ctx: {"value": ctx.storage.get("waiting", 0), "tone": "danger"} if ctx.storage.get("waiting") else None)
def orders_new(ctx, params): ...

@app.page("orders_done", "Выполненные", parent="orders")
def orders_done(ctx, params): ...

@app.page("order", "Заказ", parent="orders", hidden=True)    # карточка: подсвечивает «Заказы»
def order_card(ctx, params): ...

# Раздел «Каталог» с третьим уровнем «Импорт»
app.section("catalog", "Каталог", icon="package")

@app.page("items", "Товары", icon="box", parent="catalog")
def items(ctx, params): ...

app.section("import", "Импорт", icon="upload", parent="catalog")

@app.page("import_csv", "Из файла", parent="import")
def import_csv(ctx, params): ...

@app.page("help", "Инструкция", icon="book", bottom=True)
def help_page(ctx, params): ...

Открыта страница «Из файла» — вкладки выглядят так:

text
Обзор   Заказы 3   [Каталог]   Инструкция        Настройки   Журнал   О плагине
Товары   [Импорт]
[Из файла]

@app.menu_badge — число рядом с названием плагина в левом меню кабинета (если пользователь закрепил плагин) и на странице «Плагины → Все плагины». Например, сколько заказов ждут решения:

python
@app.menu_badge
def waiting(ctx):
    n = ctx.storage.get("waiting", 0)
    return {"value": n, "tone": "danger"} if n else None

Прежние плагины загружаются без изменений и выглядят как раньше: страницы без parent и app.section — одна строка вкладок в порядке объявления, group, hidden, bottom и badge работают как раньше. Параметр layout у WebPlugin ("sidebar", "tabs", "auto") сохранён для совместимости: все разделы показываются вкладками. Если parent указывает на несуществующий или скрытый пункт либо пункты ссылаются друг на друга по кругу, пункт встаёт на верхний уровень.

Lovsel сам добавляет служебные вкладки: «Настройки» (если объявлены app.settings), «Журнал» и «О плагине».

Название страницы уже написано на вкладке, поэтому большой заголовок показывается только у скрытых страниц. Если страница начинается с ui.header, Lovsel показывает его вместо стандартного заголовка.

Встраивание в разделы кабинета

Плагин может добавить свою кнопку или блок с данными прямо в разделы Lovsel — туда, где продавец уже работает, не открывая сам плагин. Место выбирает автор плагина:

Место (place)Где появляетсяЧто можно встроитьЧто получает плагин
"products.bulk"Список товаров площадки: продавец отмечает товары, над списком — кнопки плагиновКнопки{"ids": [...], "place": ..., "marketplace": ...}
"product.card"Карточка товара (GGsel) или кнопка «Плагины» у лота FunPay и товара PlayerokКнопки и блоки{"id": "...", "place": ..., "marketplace": ...}
"order.card"Карточка заказа или сделкиКнопки и блоки{"id": "...", "place": ..., "marketplace": ...}

id — номер товара или заказа на площадке, как его показывает Lovsel: ID товара GGsel, номер лота FunPay, ID товара Playerok, номер заказа GGsel и FunPay, ID сделки Playerok. Отметки товаров в списке появляются, только если у включённых плагинов есть кнопки для products.bulk.

Встраивания показываются на страницах площадки плагина (MANIFEST["marketplace"]), только когда плагин включён и подписка активна. У старого плагина с несколькими площадками можно указать площадку кнопки или блока параметром marketplace=.

Кнопка

app.embed_button(place, title, action, *, icon=None, tone=None, confirm=None, marketplace=None) — кнопка, которая вызывает обычное действие плагина @app.action. tone — "primary", "danger" или "neutral"; confirm — текст подтверждения перед нажатием. Ответ действия — как у любой кнопки плагина: уведомление ui.result("Готово"), переход на страницу плагина ui.result(page=...), окно ui.result(dialog=ui.dialog(...)), долгая операция с прогрессом ui.result(..., operation=op).

Пример — плагин удаления слотов: действие над выбранными товарами GGsel.

python
app.embed_button("products.bulk", "Удалить слоты", "clear_slots", icon="trash", tone="danger",
                 confirm="Слоты выбранных товаров будут удалены. Продолжить?")


@app.action("clear_slots")
def clear_slots(ctx, data):
    ids = data["ids"]                                  # ID выбранных товаров
    op = ctx.operations.start("Удаляем слоты", total=len(ids))

    def work(op):
        for product_id in ids:
            if op.cancelled:
                return
            remove_slot(ctx, product_id)               # ваша функция
            op.step(ok=True, current=product_id)
        op.finish("Слоты удалены: %d" % op.success)

    op.run_in_background(work)                         # долгую работу — в операции
    return ui.result("Удаляем слоты у %d товаров" % len(ids), operation=op)

Блок с данными

@app.embed_block(place, title=None, *, icon=None, marketplace=None) — блок в карточке товара или заказа. Функция получает (ctx, target) и возвращает список блоков ui (как страница) или None, если для этой карточки показать нечего, — тогда блок не занимает места. Кнопки внутри блока вызывают действия плагина как обычно.

Пример — SMM Prime: состояние заказа накрутки в карточке заказа и кнопка повтора.

python
@app.embed_block("order.card", "SMM Prime", icon="trending-up")
def smm_in_order(ctx, target):
    job = ctx.storage.get("jobs", {}).get(target["id"])
    if not job:
        return None                                    # заказ не наш — блока нет
    return [
        ui.keyvalue([("Услуга", job["service"]), ("Статус", job["status"]), ("Выполнено", job["done"])]),
        ui.button("Повторить", action="smm_retry", payload={"id": target["id"]}, variant="secondary"),
    ]


app.embed_button("order.card", "Открыть в SMM Prime", "smm_open", icon="external")

Блок строится, когда продавец открывает карточку, и ждёт плагин не дольше 8 секунд: тяжёлое (запросы к площадке и к поставщику) считайте заранее в обработчиках событий или в фоне и храните в ctx.storage. Ошибка плагина не ломает карточку: вместо блока Lovsel покажет короткое сообщение, а подробности — в журнале плагина.

Параметры страницы

Второй аргумент страницы — params, словарь параметров. Они хранятся в адресе страницы: обновление вкладки и кнопка «Назад» в браузере работают, ссылку на запись можно переслать.

Параметры задают:

  • кнопка ui.button("Открыть", page="rule", params={"id": rule_id});
  • ответ действия ui.result(page="rule", params={"id": rule_id});
  • строка таблицы: кнопка с page= в row_actions или row_click передаёт params["id"] — значение поля row_key строки (по умолчанию id);
  • поле поиска и фильтр в ui.toolbar — параметр с их ключом;
  • вкладки ui.tabs(..., param="tab").
python
@app.page("rule", "Правило", hidden=True)
def rule_page(ctx, params):
    rule = rules(ctx).get(str(params.get("id") or ""))
    if rule is None:
        return [ui.header("Правило не найдено", back="rules"), ui.empty("Правило удалено или ссылка устарела.")]
    ...

Всегда проверяйте, что запись из params существует: ссылка могла устареть.

python
ui.header("Новая связка", "Выберите два товара, задайте цену и срок.",
          actions=[ui.button("Все связки", page="bundles", variant="ghost")],
          back="bundles",
          badge=ui.badge("Черновик", "warning"),
          eyebrow="Связки")

back — id страницы или кнопка; badge — бейдж рядом с заголовком; eyebrow — подпись над заголовком мелкими заглавными. Главная кнопка страницы — одна, с variant="primary".

Раскладка

БлокНазначение
ui.card(title, children, description=, actions=, footer=, tone=, flush=, id=, image=, image_ratio=, glow=, icon=)Карточка — основной контейнер. actions — кнопки в шапке, footer — кнопки в подвале, tone — подсветка (warning, danger, info, success, accent), flush=True — таблица во всю ширину карточки, image — обложка над заголовком (см. «Изображения»), glow=True — мягкое свечение акцента сверху (одна главная карточка на странице), icon — значок в плитке слева от заголовка.
ui.grid(children, columns=2, widths=[2, 1])Сетка из 1–4 колонок; widths — разная ширина колонок. На телефоне — одна колонка.
ui.section(title, children, description=, actions=)Раздел с подзаголовком без рамки.
ui.tabs([ui.tab(id, label, children, badge=, icon=)], value=, param=, style=)Вкладки внутри страницы. style="underline" — вкладки с подчёркиванием.
ui.details(title, children, description=, open=, icon=)Сворачиваемый блок: редкие настройки, пояснения, опасные действия.
ui.divider(label=)Разделитель, можно с подписью.
python
ui.grid([
    ui.card("Требуют внимания", [ui.list_view(attention, empty_text="Всё отправлено.")], icon="alert"),
    ui.card("Отправки за 14 дней", [ui.chart(days, mode="bar", label="Отправки по дням")], icon="chart"),
], columns=2, widths=[3, 2])

Вкладки. Без param содержимое всех вкладок приходит сразу и переключается без запроса к плагину. С param="tab" выбранная вкладка попадает в params["tab"], страница перерисовывается, и плагин может отдать только нужную вкладку — так делают, когда вкладки тяжёлые.

Текст

БлокНазначение
ui.text(text, tone=, size=, muted=)Абзац. Поддерживает **жирный** и ` код . size: sm, md, lg`.
ui.heading(text, level=2)Подзаголовок.
ui.alert(text, title=, tone=, action=)Заметное сообщение: info, success, warning, danger. action — кнопка справа.
ui.code(text, language=, copy=True)Код или лог моноширинным шрифтом с кнопкой «Скопировать».
ui.image(src, alt=, height=, caption=, ratio=, ...)Картинка: загруженная в Lovsel (ui.asset("имя.png")), https:// или data:image/.... Подробно — в разделе «Изображения».
ui.empty(title, description=, action=, icon=)Пустое состояние: что здесь появится и что сделать.

Пустое состояние обязательно, если данных может не быть:

python
ui.empty("Связок пока нет", "Соберите первую связку из двух товаров.", icon="layers",
         action=ui.button("Создать связку", page="new", variant="primary", icon="plus"))

Изображения

Баннеры, превью, скриншоты и картинки для инструкций загружаются в кабинете разработчика: «Разработчикам» → плагин → вкладка «Разработка» → «Изображения». В коде на картинку ссылаются по имени файла через ui.asset — Lovsel сам подставит адрес, поэтому картинка одинаково открывается у вас и у всех покупателей, на компьютере и на телефоне. Файл с тем же именем заменяет прежний: код менять не нужно.

БлокНазначение
ui.asset("banner.png")Ссылка на загруженную картинку. Подходит везде, где нужна картинка: ui.image, ui.banner, ui.gallery, ui.card(image=), ui.item(image=), ui.choice(image=), колонка таблицы с format="image". Расширение можно не писать, если имя одно.
ui.image(src, alt=, height=, caption=, ratio=, fit=, width=, zoom=, align=)Картинка. caption — подпись, ratio — пропорции рамки (21:9, 16:9, 3:2, 4:3, 1:1, 3:4), height — высота на компьютере в пикселях, fit="contain" — показать целиком без обрезки, width — наибольшая ширина, align="center" — по центру. По нажатию открывается во весь экран (zoom=False — выключить).
ui.banner(image, title=, description=, actions=, eyebrow=, ratio="21:9", align=, overlay=True)Баннер во всю ширину: картинка, заголовок, текст и кнопки поверх неё. На телефоне баннер становится выше, чтобы текст и кнопки поместились. Без title и description — просто широкая картинка.
ui.gallery(images, columns=3, ratio="4:3", fit=, zoom=True)Галерея: превью, скриншоты, шаги инструкции. Элементы — ui.asset(...), ссылки или {"src": ..., "caption": ...}. На телефоне — две колонки; по нажатию картинки листаются во весь экран.
ui.card(..., image=ui.asset("cover.png"), image_ratio="16:9")Карточка с обложкой.
ui.item(..., image=ui.asset("lot.png"))Миниатюра слева в ui.list_view.
python
@app.page("guide", "Инструкция", icon="book", bottom=True)
def guide(ctx, params):
    return [
        ui.banner(ui.asset("banner.png"), "Автовыдача за 3 шага", "Настройка займёт пару минут.",
                  actions=[ui.button("Начать", page="rules", variant="primary")]),
        ui.gallery([
            {"src": ui.asset("step-1.png"), "caption": "1. Выберите товар"},
            {"src": ui.asset("step-2.png"), "caption": "2. Напишите текст"},
            {"src": ui.asset("step-3.png"), "caption": "3. Включите правило"},
        ], columns=3, ratio="16:9"),
        ui.image(ui.asset("result.png"), caption="Так покупатель увидит сообщение", width=520),
    ]

Ограничения: PNG, JPEG, WebP или GIF, до 2 МБ на файл, до 40 файлов и 20 МБ на плагин. SVG не принимается — в нём может быть скрипт. Тип файла проверяется по содержимому, а не по расширению. Если картинку с указанным именем не загрузили, вместо неё будет заглушка «Изображение не загружено» — тестовый стенд покажет такие места заранее (см. «Проверка страниц»).

Значения

Значения для ячеек таблиц, ui.keyvalue и показателей:

ФункцияРезультат
ui.badge(label, tone, value=, dot=)Бейдж статуса. value — код для фильтров таблицы («on», «off»).
ui.value(v, format=, currency=, subtitle=)Значение с форматом: ui.value(2450, format="money", currency="RUB").
ui.link(label, href)Внешняя ссылка https://.
ui.money(value, currency)Готовая строка суммы: ui.money(1234.5, "USD") → «1 234,50 $».
ui.duration_text(value)Срок по-русски: {"value": 7, "unit": "days"} → «7 дней», "12h" → «12 часов».

Валюту берите из данных (order["currency"], ctx.ggsel.currency()), никогда не пишите знак рубля в коде: у площадок и заказов разная валюта.

Показатели

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

python
ui.stats([
    ui.stat("Выручка за 30 дней", 16942, format="money", currency="RUB", delta=8,
            spark=revenue_by_day[-14:], glow=True, hint="за неделю 4 553 ₽"),
    ui.stat("Аренд за 30 дней", 58, icon="receipt", spark=rentals_by_day[-14:]),
    ui.stat("Идут сейчас", 43, icon="clock", page="rentals"),
    ui.stat("Возвраты", 2, icon="alert", delta=50, up_is_good=False),
])

ui.stat(label, value, hint=, tone=, icon=, delta=, format=, currency=, page=, spark=, glow=, up_is_good=):

ПараметрЧто делает
formatnumber, money (с currency), percent. Текстовое значение («3 из 4») показывается как есть.
deltaИзменение в процентах: бейдж «+8%» зелёный, «−5%» красный. up_is_good=False — для возвратов и ошибок рост красный.
sparkРяд чисел для линии динамики в углу плитки (до 60 точек, на телефоне линия скрыта).
glow=TrueМягкое свечение акцента за цифрой — один главный показатель на странице.
pageОткрыть страницу плагина по нажатию на плитку.
toneЦвет цифры: success, warning, danger, info.

ui.stats(items, columns=) — число колонок, по умолчанию до четырёх (на телефоне — две).

Графики

ui.chart использует те же графики, что и «Аналитика» Lovsel: подсказка при наведении, целые деления для количеств, светящаяся точка «сейчас», если ряд доходит до сегодняшнего дня, и три вида — «Гладкий» (плавные линии с заливкой), «Острый» (ломаные) и «Столбцы».

python
ui.card("Выручка", [
    ui.chart(series=[
        ui.series("new", "Новые покупатели", [{"date": "2026-10-01", "value": 1200}, ...]),
        ui.series("repeat", "Повторные покупки", [{"date": "2026-10-01", "value": 800}, ...], color="info"),
    ], format="money", currency="RUB", label="Выручка", switch=True, id="revenue"),
], icon="trending-up")

ui.chart(data=, kind=, label=, format=, currency=, height=, compare=, series=, mode=, switch=, id=):

ПараметрЧто делает
dataОдин ряд по датам: [{"date": "2026-09-01", "value": 120}, …].
seriesНесколько линий: ui.series(key, label, data, color=). Над графиком появляются переключатели линий.
modeВид: "smooth" (по умолчанию), "linear" или "bar".
switch=TrueПереключатель вида справа над графиком; выбор запоминается в браузере, если задан id.
compareВторой ряд пунктиром — прошлый период (для графика из одной линии).
formatnumber или count — целые деления шкалы; money — суммы с currency.
heightВысота в пикселях (120–480).

ui.series(key, label, data, color="accent") — линия графика. color: accent (цвет площадки плагина), ggsel, funpay, playerok (цвета площадок, как в аналитике), neutral, info, warning, danger.

Другие виды — через kind:

kindДанные
"bars"Горизонтальные полосы: [{"label": "Steam", "value": 12, "hint": "…"}, …].
"donut"Доли: [{"label": "5★", "value": 70}, …].
"area", "line", "bar"Прежние имена видов: работают как mode="smooth", "linear", "bar".

Таблицы

Таблица — основной способ показать список объектов: связки, правила, заказы, товары. Сортировка по колонкам, поиск, вкладки-фильтры, переключатели в строках, кнопки и меню «⋯», выбор строк, страницы и выбор видимых колонок. На телефоне строки превращаются в карточки: название, переключатель, значения парами и кнопки — без прокрутки вбок.

python
edit = ui.editor("save_bundle", [ui.field.text("name", "Название"), ui.field.money("price", "Цена", currency="RUB")],
                 title="Связка", delete="delete_bundle")

ui.card(None, [ui.table(
    [ui.column("name", "Связка", subtitle="pair"),
     ui.column("price", "Цена", format="money", align="right"),
     ui.column("term_text", "Срок"),
     ui.column("sales", "Аренд", format="number", align="right"),
     ui.column("enabled", "Включена", format="switch", action="toggle_bundle", sortable=False)],
    rows,
    searchable=True, search_placeholder="Название или товар",
    filters=[ui.filter("status", options=[("on", "Включены"), ("off", "Выключены")], style="tabs")],
    row_actions=[ui.button("Изменить", variant="ghost", icon="edit", dialog=edit)],
    row_menu=[ui.button("Удалить", "delete_bundle", icon="trash", danger=True, confirm="Удалить связку?")],
    toolbar=[ui.button("Создать связку", page="new", variant="primary", icon="plus")],
    empty_state=ui.empty("Связок пока нет", "Соберите первую связку.", icon="layers",
                         action=ui.button("Создать связку", page="new", variant="primary")),
    footer="3 связки · 2 включены",
)], flush=True)
ПараметрЧто делает
searchable, search_placeholderПоиск по всем колонкам прямо в браузере.
filtersФильтры ui.filter по полям строк. style="tabs" — вкладки со счётчиками над таблицей, "select" — кнопка-фильтр рядом с поиском. Бейдж фильтруется по своему value.
row_actionsКнопки в конце строки. Кнопка с action получает строку в data["row"]; кнопка с page открывает страницу с params["id"]; кнопка с dialog=ui.editor(...) открывает редактор, заполненный значениями строки (см. «Редактор объекта»).
row_menuПункты меню «⋯» строки — для второстепенных и опасных действий.
row_clickКнопка, которая срабатывает по нажатию на строку: обычно открывает карточку записи.
selectable, bulk_actionsГалочки и действия над выбранными строками: data["ids"] и data["rows"].
row_keyПоле с идентификатором строки (по умолчанию id).
footer, footer_actionsИтог и кнопки под таблицей.
toolbarКнопки справа от поиска.
column_toggleМеню «Колонки». По умолчанию есть, если колонок больше четырёх.
page_sizeСтрок на странице (5–200, по умолчанию 20).
dense=TrueКомпактные строки.
title, description, empty_textЗаголовок, пояснение и текст пустой таблицы.
empty_stateui.empty(...) с пояснением и кнопкой, когда строк нет совсем.

ui.column(key, label, align=, format=, width=, currency=, subtitle=, sortable=, hidden=, nowrap=, action=, confirm=):

formatКак показывается значение
textТекст (по умолчанию).
moneyСумма: валюта из поля currency строки или параметра currency колонки.
number, percentЧисло с разрядами, проценты.
date, datetime, relativeДата, дата и время, «3 дн назад». Значение — ISO-строка.
badge, statusБейдж ui.badge; status — бейдж с точкой.
linkСсылка ui.link или https://-строка.
codeМоноширинный текст: ключи, номера.
image, avatarКартинка; аватар с инициалами из текста.
tagsСписок строк бейджами.
progressПолоска прогресса, значение 0–100.
bool«Да» или «Нет».
switchПереключатель прямо в строке. Нужен action: действие получает {"row": строка, "key": key, "value": True/False}. Переключатель меняется сразу, а если действие вернуло ошибку — возвращается назад. confirm — спросить перед переключением.

subtitle — поле строки для второй строки ячейки: «Аренда Steam → Аккаунт Steam». hidden=True — колонка скрыта, пока пользователь не включит её в меню «Колонки».

python
@app.action("toggle_bundle")
def toggle_bundle(ctx, data):
    bundles = ctx.storage.get("bundles", {})
    bundle = bundles[data["row"]["id"]]
    bundle["enabled"] = data["value"]
    ctx.storage.set("bundles", bundles)
    return ui.result("Связка включена" if data["value"] else "Связка выключена")

Поиск и фильтры страницы

Фильтры таблицы работают в браузере с уже полученными строками. Если данных много и отбирать их должен плагин, используйте панель ui.toolbar: её поиск и фильтры меняют параметры страницы, и страница перерисовывается.

python
@app.page("history", "История", icon="history")
def history_page(ctx, params):
    q = str(params.get("q") or "").lower()
    status = str(params.get("status") or "")
    items = [h for h in history(ctx) if (not status or h["status"] == status) and q in h["order_id"].lower()]
    return [
        ui.toolbar([
            ui.search("q", placeholder="Номер заказа, товар или покупатель"),
            ui.filter("status", "Статус", [("sent", "Отправлено"), ("error", "Ошибка")]),
            ui.button("Очистить историю", "clear_history", variant="ghost", icon="trash", confirm="Очистить?"),
        ]),
        ui.table(COLUMNS, rows_of(items), empty_text="Ничего не найдено."),
    ]

ui.search(param="q", placeholder=) меняет параметр с небольшой задержкой при вводе. ui.filter(key, label, options, style=) в панели — кнопка-фильтр или вкладки (style="tabs"). Кнопки панели прижимаются вправо.

Варианты выбора в ui.filter и полях: "a", ("a", "Подпись") или {"value": "a", "label": "Подпись", "hint": "пояснение", "count": 3}.

Поиск товаров по мере ввода внутри формы — это поле `field.picker`.

Списки, ленты и этапы

БлокНазначение
ui.keyvalue({...} или [(label, value)], columns=1)«Параметр — значение». Значение — текст, ui.badge, ui.value, ui.link.
ui.list_view([ui.item(...)], empty_text=)Список: «Требуют внимания», «Скоро закончатся», последние события.
ui.timeline([ui.item(..., time=)], empty_text=)Лента по времени: история заказа, журнал действий.
ui.steps(["Оплачен", "Подтверждён", "Отзыв"], current=1)Этапы процесса; current — номер текущего с нуля.

ui.item(title, description=, badge=, tone=, meta=, action=, actions=, icon=, time=, image=) — элемент списка и ленты (image — миниатюра слева вместо иконки):

python
ui.list_view([
    ui.item("Game Pass на месяц", "Заказ #90002 · dmitry", meta="до 19.10 00:56", icon="clock",
            badge="GGsel", tone="info", action=ui.button("Открыть", page="rental", params={"id": "r1"}, size="sm")),
], empty_text="Сейчас нет идущих аренд.")

Кнопки и действия

ui.button(label, action=, payload=, page=, params=, href=, dialog=, variant=, icon=, confirm=, danger=, disabled=, size=):

  • action — вызвать действие плагина @app.action с payload;
  • page + params — открыть страницу плагина;
  • href — внешняя ссылка https://;
  • dialog — открыть окно ui.dialog или редактор ui.editor без обращения к плагину;
  • confirm — текст подтверждения перед действием;
  • variant: primary (цвет площадки), secondary, outline, ghost, link, soft, danger; size="sm" — маленькая кнопка.
БлокНазначение
ui.actions([buttons], align=)Ряд кнопок; align: start, end, center, between (первая слева, остальные справа).
ui.menu(label, [buttons], icon=, variant=)Кнопка с выпадающим списком действий.
ui.toggle(key, label, value, action=, description=, confirm=)Строка с переключателем: сразу вызывает действие с {"key": key, "value": True/False}. Если действие упало, переключатель вернётся назад.

Действие получает data: payload кнопки, значения формы, строку таблицы (row) или выбранные строки (ids, rows). Что можно вернуть:

РезультатЭффект
None или "Текст"Страница обновится; строка — уведомление «успешно».
ui.result("Текст", tone="info")Уведомление: success, info, warning, danger.
ui.result(page="rule", params={"id": 5})Перейти на страницу плагина.
ui.result(dialog=ui.dialog(...))Открыть окно (или dialog=ui.editor(...) — редактор).
ui.result(operation=op)Показать прогресс запущенной операции.
ui.result(download=ctx.download(...))Скачать файл.
ui.result(errors={"price": "…"})Ошибки у полей формы (то же, что raise ValidationError).
ui.result(values={...})Новые значения полей формы или мастера.
ui.result(close=True)Закрыть окно, из которого вызвано действие.
ui.result(refresh=False)Не перерисовывать страницу.
ui.choices([...])Варианты для поиска в `field.picker`.

ui.dialog(title, blocks, description=, size=, side=) — окно: size — sm, md, lg, xl; side=True — панель справа (удобно для карточки записи поверх таблицы).

python
@app.action("show_message")
def show_message(ctx, data):
    row = data.get("row") or {}
    return ui.result(refresh=False, dialog=ui.dialog("Заказ " + row["order_id"], [
        ui.keyvalue([("Покупатель", row["buyer"]), ("Статус", row["status"])]),
        ui.chat([{"text": row["text"], "author": "Вы", "direction": "out", "time": "12:40"}]),
    ], side=True))

Формы

python
ui.form("add_slot", [
    ui.field.section("Товар"),
    ui.field.text("title", "Название", required=True, width="half"),
    ui.field.select("region", "Регион", [("ar", "Аргентина"), ("tr", "Турция")], default="ar", width="half"),
    ui.field.radio("mode", "Режим", [{"value": "auto", "label": "Автоматически", "hint": "По расписанию"},
                                      {"value": "manual", "label": "Вручную"}], default="auto"),
    ui.field.money("price", "Цена", currency="RUB", width="half"),
    ui.field.duration("term", "Срок аренды", units=("hours", "days"), default="7d", width="half"),
    ui.field.number("markup", "Наценка", min=0, max=200, default=15, suffix="%",
                    show_if={"field": "mode", "equals": "auto"}),
    ui.field.tags("stop_words", "Стоп-слова", placeholder="Слово и Enter"),
    ui.field.file("keys", "Файл с ключами", accept=".txt", max_mb=5),
], submit="Добавить", action="add_slot",
   secondary=[ui.button("Отмена", page="slots", variant="ghost")])
ПолеЗначение в действии
field.text(key, label, placeholder=, secret=, max_length=, prefix=, suffix=)Строка. secret=True — пароль или ключ: не отдаётся в браузер.
field.textarea(key, label, rows=, max_length=)Многострочный текст.
field.number(key, label, min=, max=, step=, prefix=, suffix=)Число.
field.money(key, label, currency=, min=0, max=)Сумма со знаком валюты справа. Число до копеек: «299,5» → 299.5.
field.duration(key, label, units=("hours", "days"), unit="days")Срок: число и единица (minutes, hours, days, weeks). Приходит словарём {"value": 7, "unit": "days", "minutes": 10080}. default — словарь, строка "12h" / "7d" или число в unit.
field.switch(key, label)True или False.
field.select(key, label, options, searchable=, placeholder=)Строка. searchable=True — список с поиском (сотни вариантов).
field.radio(key, label, options, cards=True)Строка. Карточки с пояснением из hint варианта.
field.checkboxes(key, label, options)Список строк.
field.tags(key, label)Список фраз: ключевые слова, стоп-слова.
field.template(key, label, variables=)Текст сообщения с переменными и предпросмотром (см. «Сообщения покупателю»).
field.picker(key, label, source=, items=, mode=, multiple=, min=, max=)Выбор товаров поиском или из каталога — id строкой или списком id (см. «Выбор товаров»).
field.date(key, label)Дата ГГГГ-ММ-ДД.
field.file(key, label, accept=, max_mb=)Объект UploadedFile: name, size, mime, content, text(), lines().
field.hidden(key, value)Скрытое значение, например id записи.
field.section(title, description=)Подзаголовок внутри длинной формы.

Общие параметры полей: hint, placeholder, required, default, disabled, width="half" (два поля в ряд), show_if={"field": "mode", "equals": "manual"} или {"field": "mode", "in": ["a", "b"]} — показывать поле, только если другое поле равно значению. Скрытые условием поля не проверяются и не отправляются.

ui.form(id, fields, submit=, action=, values=, description=, danger=, secondary=, columns=2): values — текущие значения, secondary — кнопки слева от «Сохранить», columns=1 — все поля в одну колонку.

Lovsel проверяет обязательные поля, числа, суммы, сроки и выбор ещё до вызова действия и приводит значения к типам. Ошибку, которую видит только плагин, покажите у поля:

python
if price <= 0:
    raise ValidationError({"price": "Цена должна быть больше нуля"})

Не просите пользователя вводить данные по одному сообщению («введите название», «теперь цену»): одна форма — одно действие.

Выбор товаров

field.picker — выбор одного или нескольких объектов: товаров площадки плагина (товары GGsel, лоты FunPay или товары Playerok) и своих позиций. Два вида: поиск — поле, результаты появляются по мере ввода; каталог — сетка карточек с картинкой, названием, ценой и меткой. Выбранное показывается карточками над полем, их можно убрать или заменить. Работает в формах, мастерах и редакторе.

python
ui.field.picker("product_a", "Товар продажи", source="search_products", mode="both", required=True,
                exclude=["product_b"], hint="Что покупатель оплачивает на площадке.")

ui.field.picker("pair", "Два товара", source="search_products", mode="catalog", multiple=True, min=2, max=2)
ПараметрЧто делает
sourceДействие плагина, которое ищет варианты (см. ниже). Без него — поиск по списку items прямо в браузере.
itemsГотовые варианты ui.choice(...) — когда их немного.
mode"search" — поиск списком, "catalog" — сетка карточек, "both" — переключатель «Список / Каталог».
multiple, min, maxНесколько вариантов: значение — список id. min=2, max=2 — ровно два; после выбора нужного числа список сворачивается.
selectedУже выбранные ui.choice(...) — когда форма открыта для изменения.
excludeКлючи других полей-выборов: выбранное там здесь недоступно с пометкой «Уже выбран в другом поле».
columnsКолонок каталога на широком экране: 2, 3 или 4. На телефоне — две.
empty_text, placeholderТекст, если ничего не найдено, и подсказка в поле поиска.

Действие поиска получает {"query": текст, "field": ключ поля, "page": 1, "values": значения формы} и возвращает ui.choices(items, total=, note=). Запрос уходит через 0,3 секунды после ввода; при пустом запросе показывайте первую страницу каталога. Если total больше показанного, внизу появится «Показать ещё» — придёт page=2.

python
@app.action("search_products")
def search_products(ctx, data):
    query = str(data.get("query") or "").lower()
    page = int(data.get("page") or 1)
    found = [p for p in ctx.ggsel.products() if query in p["title"].lower()]
    return ui.choices([ui.choice("ggsel:%s" % p["id"], p["title"], subtitle="GGsel · %s" % p["id"],
                                 price=p["price"], currency=p["currency"], marketplace="ggsel",
                                 badge=None if p["active"] else "Не активен", tone="warning")
                       for p in found[(page - 1) * 24:page * 24]], total=len(found))

ui.choice(id, title, subtitle=, image=, price=, currency=, badge=, tone=, marketplace=, meta=, disabled=, hint=) — вариант выбора. image — ui.asset(...) или https://-ссылка; без картинки показывается плитка с буквами названия и значком площадки marketplace. disabled=True — вариант виден, но выбрать его нельзя (hint объясняет почему).

Lovsel не придумывает данные площадок: в каталоге только то, что вернул плагин. У товаров GGsel, лотов FunPay и товаров Playerok в SDK нет картинок — передавайте image, если храните свои (например, загруженные через ui.asset).

Редактор объекта

ui.editor — панель справа (или окно) для создания и изменения одного объекта: связки, правила, товара. Поля, ошибки под полями, «Сохранить», «Отмена» и «Удалить» с подтверждением; после успешного сохранения панель закрывается, а страница обновляется.

python
edit = ui.editor("save_bundle", [
    ui.field.text("name", "Название связки", max_length=80),
    ui.field.money("price", "Цена", currency="RUB", required=True, width="half"),
    ui.field.duration("term", "Срок", units=("hours", "days", "weeks"), required=True, width="half"),
], title="Связка", delete="delete_bundle", delete_label="Удалить связку")

ui.table(columns, rows, row_actions=[ui.button("Изменить", variant="ghost", icon="edit", dialog=edit)])

ui.editor(id, fields, title=, action=, values=, description=, submit=, delete=, delete_label=, delete_confirm=, row_key="id", side=True, size="md"):

  • Кнопка с dialog=ui.editor(...) в строке таблицы заполняет поля значениями этой строки (по ключам полей) и передаёт в действие data["id"] = row[row_key] — отдельный запрос к плагину не нужен.
  • action (по умолчанию id) — сохранение: получает значения полей и id. raise ValidationError({...}) — ошибки под полями, панель остаётся открытой.
  • delete — действие удаления: подтверждение delete_confirm, затем действие получает data["id"].
  • Вне таблицы передайте текущие значения и id в values: ui.editor(..., values={"id": rule_id, "name": ...}). Так же — если редактор открывает действие: return ui.result(dialog=ui.editor(..., values=...), refresh=False).
  • side=False — окно по центру вместо панели; size: sm, md, lg, xl.
  • Поля идут в одну колонку; два поля с width="half" встают в ряд.
python
@app.action("save_bundle")
def save_bundle(ctx, data):
    bundles = ctx.storage.get("bundles", {})
    bundle = bundles.get(str(data.get("id")))
    if bundle is None:
        raise ValidationError("Связка не найдена — обновите страницу")
    if data["price"] <= 0:
        raise ValidationError({"price": "Цена должна быть больше нуля"})
    bundle.update(name=data["name"] or bundle["name"], price=data["price"], term=data["term"])
    ctx.storage.set("bundles", bundles)
    return ui.result("Связка сохранена")

Панель настроек

ui.settings — настройки по группам: название и пояснение группы слева, поля в карточке справа (на телефоне — друг под другом). Подходит для страницы «Настройки» плагина, когда настройки хранятся в ctx.storage и их много.

python
@app.page("settings", "Настройки", icon="settings")
def settings_page(ctx, params):
    return [ui.settings("save_settings", [
        ui.settings_group("Уведомления", [
            ui.field.switch("notify_sale", "Сообщать о новой аренде"),
        ], description="Что Lovsel сообщит вам."),
        ui.settings_group("Сообщение покупателю", [
            ui.field.switch("message_on", "Отправлять сообщение после оплаты"),
            ui.field.template("message", "Текст", variables=VARIABLES,
                              show_if={"field": "message_on", "equals": True}),
        ]),
    ], values=ctx.storage.get("settings", {}))]


@app.action("save_settings")
def save_settings(ctx, data):
    ctx.storage.set("settings", dict(ctx.storage.get("settings", {}), **data))
    return ui.result("Настройки сохранены", refresh=False)

ui.settings(id, groups, action=, values=, autosave=True, description=, submit=), ui.settings_group(title, fields, description=, icon=):

  • autosave=True — каждое изменение сохраняется само: переключатели и выбор сразу, текст — через секунду после ввода. Над панелью видно «Сохраняем…», «Сохранено только что» или «Не сохранено — проверьте поля». Страница при автосохранении не перерисовывается, уведомлений нет.
  • autosave=False — при изменениях внизу появляется панель «Есть несохранённые изменения» с кнопками «Отменить» и «Сохранить».
  • Действие (по умолчанию id) получает значения всех видимых полей панели. Поле, скрытое условием show_if, не приходит — сохраняйте прежнее значение, как в примере выше.

Чем отличается от app.settings([...]): app.settings — простые настройки в ctx.settings, страницу «Настройки» Lovsel строит сам (с полями-секретами, которые не отдаются в браузер). ui.settings — ваша страница, ваше хранение и группы.

Мастер

Пошаговая форма для создания сложной записи: «1 · Товар продажи → 2 · Товар выдачи → 3 · Цена и срок → 4 · Проверка». Над шагами — полоса прогресса из отрезков, пройденные шаги открываются нажатием, шаги сменяются плавно.

python
ui.wizard("create_bundle", [
    ui.step("a", "Товар продажи", [
        ui.field.picker("product_a", "Товар продажи", source="search_products", mode="both", required=True,
                        exclude=["product_b"]),
    ], description="Что покупатель оплачивает на площадке."),
    ui.step("b", "Товар выдачи", [
        ui.field.picker("product_b", "Товар выдачи", source="search_products", mode="both", required=True,
                        exclude=["product_a"]),
    ]),
    ui.step("terms", "Цена и срок", [
        ui.field.text("name", "Название связки"),
        ui.field.money("price", "Цена", currency="RUB", required=True, width="half"),
        ui.field.duration("term", "Срок", units=("hours", "days", "weeks"), default="7d", width="half"),
    ]),
    ui.step("review", "Проверка", review=True),
], validate="check_step", submit="Создать связку", cancel=ui.button("Отмена", page="bundles", variant="ghost"))
  • «Продолжить» проверяет обязательные поля текущего шага.
  • validate — действие, которое вызывается на каждом шаге с data["__step"] = id шага и значениями всех пройденных шагов. Оно может raise ValidationError({...}) или вернуть ui.result(values={...}), чтобы заполнить следующие шаги (например, подставить название и цену выбранного товара).
  • review=True — итоговый шаг: Lovsel сам покажет все введённые значения; выбранные товары — карточками с ценой, сроки — словами («7 дней»).
  • На последнем шаге вызывается action (по умолчанию id мастера) со всеми значениями. Обычно он возвращает ui.result("Создано", page="item", params={"id": new_id}). Если действие не переходит на другую страницу, мастер очищается и начинается с первого шага — для следующей записи.

ui.step(id, title, fields, description=, blocks=, review=), ui.wizard(id, steps, action=, submit=, values=, validate=, description=, cancel=).

Сообщения покупателю

Для плагинов, которые пишут покупателям, есть редактор шаблона и предпросмотр в стиле чата площадки.

python
VARIABLES = [
    ui.var("buyer", "Имя покупателя", "Алексей"),
    ui.var("product", "Название товара", "Telegram · подписчики"),
    ui.var("order_id", "Номер заказа", "10842"),
]

ui.form("save_text", [
    ui.field.template("text", "Текст сообщения", variables=VARIABLES, required=True),
], values={"text": current_text}, submit="Сохранить текст")
  • ui.var(key, label, example) — переменная: {buyer} в тексте, «Алексей» в предпросмотре.
  • field.template(key, label, variables=, rows=, preview=True, preview_title=) — поле с чипами переменных (вставляются туда, где стоит курсор) и живым предпросмотром рядом.
  • ui.message_preview(text, variables=, title=, author=, note=, marketplace=) — готовое сообщение глазами покупателя.
  • ui.chat(messages, title=, marketplace=) — переписка: [{"text": "…", "author": "Покупатель", "direction": "in", "time": "12:40"}]. Исходящие сообщения ("out") окрашены в цвет площадки.

Подставлять переменные в настоящий текст плагин должен сам (text.replace("{buyer}", name)): Lovsel лишь показывает пример.

Прогресс, операции и журнал

БлокНазначение
ui.progress(value, max, label=)Статичный прогресс: «14 из 50».
ui.progress(operation=op.id)Живой прогресс запущенной операции.
ui.operations(limit=10)Все операции плагина с прогрессом и кнопкой «Остановить».
ui.logs(limit=100, filters=True)Журнал плагина (ctx.log) с поиском и фильтром по уровню.

Ошибки обработчиков Lovsel записывает в журнал сам — с трассировкой до строки вашего файла. Автор видит её во вкладке «Журнал» и в кабинете разработчика.

Иконки

Иконки задаются именем: activity, alert, arrow-left, arrow-right, bar-chart, bell, book, bot, box, calendar, card, cart, chart, check, check-circle, clock, copy, dashboard, download, edit, external, eye, file, file-text, filter, gift, globe, hash, heart, help, history, image, inbox, info, key, layers, layout-dashboard, link, list, list-checks, lock, mail, message, message-square, messages, money, more, order, package, panel, pause, percent, play, plus, receipt, refresh, rocket, search, send, settings, shield, sliders, sparkles, star, stop, store, table, tag, trash, trend, trending-up, truck, upload, user, users, wallet, wand, x, x-circle, zap. Можно указать эмодзи.

Проверка страниц

Тестовый стенд отрисовывает каждую страницу и проверяет блоки: неизвестный тип блока, кнопку без действия, повторяющийся ключ поля, картинку с неверным адресом, выбор товаров без source, переключатель в таблице, редактор или настройки, которые вызывают несуществующее действие, — он покажет с путём до места ошибки. Если передать папку с картинками плагина (assets="images/"), стенд проверит, что каждая ui.asset("имя") ссылается на существующий файл.

bash
python -m lovsel_sdk.testing my_plugin.py
python
from lovsel_sdk.testing import load_plugin

plugin = load_plugin("my_plugin.py", products=[{"id": 101, "title": "Ключ Steam", "price": 199}],
                     assets="images/")
blocks = plugin.page("bundles")                 # ValueError, если блок описан с ошибкой
found = plugin.action("search_products", {"query": "steam"})["items"]

# Отправить форму, мастер или редактор так, как это делает кабинет: те же проверки и приведение типов
editor = blocks[0]["children"][0]["row_actions"][0]["dialog"]["blocks"][0]
plugin.submit("save_bundle", editor["fields"], {"name": "Неделя", "price": "299,5", "term": "7d"},
              payload={"id": "b1"})

plugin.submit(action, fields, values, payload=) проверяет обязательные поля, суммы, сроки и выбор теми же правилами, что сервер Lovsel: ошибка — ValidationError с errors по ключам полей.

Встраивания проверяются так же: plugin.embed(place, id) возвращает блоки @app.embed_block для карточки — список пар (заголовок, блоки), пустой, если плагину нечего показать, — и проверяет, что действия кнопок app.embed_button объявлены.

python
assert plugin.embed("order.card", "123") == []          # заказ не наш — блока нет
title, blocks = plugin.embed("order.card", "456")[0]
plugin.action("clear_slots", {"ids": ["101", "102"], "place": "products.bulk", "marketplace": "ggsel"})

Готовые образцы

Обзор. ui.stats на четыре показателя со spark и одним glow, карточка с ui.chart(series=..., switch=True), «Скоро закончатся» или «Требуют внимания» в ui.list_view. Если данных ещё нет — одна карточка glow=True с ui.empty и главной кнопкой.

Список объектов. ui.card(None, [ui.table(...)], flush=True): поиск, вкладки-фильтры, колонка-переключатель «Включена», кнопка «Изменить» с dialog=ui.editor(...), «Удалить» в меню «⋯» с подтверждением, «Создать» в toolbar, empty_state с той же кнопкой.

Создание записи. Кнопка «Создать» ведёт на скрытую страницу с ui.header(back=...) и ui.wizard: выбор товаров field.picker, параметры, проверка. Действие мастера возвращает ui.result("Создано", page="items").

Список и карточка записи. Таблица с row_click=ui.button("Открыть", page="item"). Скрытая страница @app.page("item", ..., hidden=True) читает params["id"], начинается с ui.header(..., back="items") и показывает ui.keyvalue, историю ui.timeline и опасные действия в ui.details.

Настройки. Отдельная вкладка с ui.settings(..., autosave=True) и группами по смыслу.

Сценарии во вкладках. ui.tabs с вкладкой на сценарий: в каждой ui.toggle «Включено» и форма с field.template. Так устроены приветствие, просьба об отзыве и ответ на отзыв в примере для FunPay.

Журнал с поиском. ui.toolbar([ui.search(...), ui.filter(...)]) и таблица, которую плагин отбирает по params; нажатие на строку открывает ui.dialog(..., side=True) с подробностями.

Полные примеры

  • Связки товаров (в духе KOSell) — плагин GGsel: выбор двух товаров поиском или из каталога, мастер, список с переключателями и редактором, аренды по заказам, показатели, график выручки, настройки с автосохранением. Файл docs/examples/bundle_rent_example.py, пошаговый разбор — в уроке «Создаём плагин как KOSell: связки товаров».
  • GGsel — «Благодарности покупателям»: правила с мастером, шаблон с предпросмотром, история с поиском и повторной отправкой. Файл docs/examples/ggsel_thanks.py, разбор — в статье «Плагины для GGsel».
  • FunPay — «Отзывы и напоминания»: сценарии во вкладках, заказы с этапами, отзывы с фильтрами, переписка в окне сбоку. Файл docs/examples/funpay_reviews.py, разбор — в статье «Плагины для FunPay».

Примеры можно скачать кнопкой «Пример плагина» в документации, загрузить в «Плагины → Разработчикам» и открыть у себя — без публикации в магазине.