Плагин описывает, что показать: разделы, показатели, графики, списки, формы, кнопки и действия. Как это выглядит, решает Lovsel. Каждая функция ui.* возвращает обычный словарь, а кабинет отрисовывает его теми же компонентами, что и встроенные разделы: плитки показателей с линией динамики, графики как в «Аналитике», таблицы с переключателями в строках, выбор товаров из каталога, редактор в панели справа, настройки с автосохранением и пошаговые мастера. Поэтому любой плагин выглядит частью Lovsel — в чёрной теме с зелёными акцентами и в светлой теме, на компьютере и на телефоне. HTML, CSS и JavaScript писать не нужно.
Структура файла, манифест, события и публикация описаны в руководстве «Как создать плагин», а полный плагин с нуля — в уроке «Создаём плагин как KOSell: связки товаров». API площадок — в разделах «Плагины для GGsel», «Плагины для FunPay» и «Плагины для Playerok».
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 (см. «Графики»).
Навигация плагина
Каждая страница — функция, которая возвращает список блоков. Страницы показываются вкладками под шапкой плагина. Порядок объявления — порядок вкладок. Второй боковой панели нет, а в левом меню кабинета — только сами плагины (закреплённые пользователем), без их разделов.
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") оставлен для совместимости и на вид не влияет.
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): ...Открыта страница «Из файла» — вкладки выглядят так:
Обзор Заказы 3 [Каталог] Инструкция Настройки Журнал О плагине
Товары [Импорт]
[Из файла]Счётчик у плагина
@app.menu_badge — число рядом с названием плагина в левом меню кабинета (если пользователь закрепил плагин) и на странице «Плагины → Все плагины». Например, сколько заказов ждут решения:
@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.
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: состояние заказа накрутки в карточке заказа и кнопка повтора.
@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").
@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 существует: ссылка могла устареть.
Шапка страницы
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=) | Разделитель, можно с подписью. |
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=) | Пустое состояние: что здесь появится и что сделать. |
Пустое состояние обязательно, если данных может не быть:
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. |
@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()), никогда не пишите знак рубля в коде: у площадок и заказов разная валюта.
Показатели
Ряд плиток — те же, что на главной кабинета: подпись мелкими заглавными, крупная цифра, которая плавно досчитывается при открытии, бейдж изменения, пояснение и маленькая линия динамики.
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=):
| Параметр | Что делает |
|---|---|
format | number, 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: подсказка при наведении, целые деления для количеств, светящаяся точка «сейчас», если ряд доходит до сегодняшнего дня, и три вида — «Гладкий» (плавные линии с заливкой), «Острый» (ломаные) и «Столбцы».
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 | Второй ряд пунктиром — прошлый период (для графика из одной линии). |
format | number или 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". |
Таблицы
Таблица — основной способ показать список объектов: связки, правила, заказы, товары. Сортировка по колонкам, поиск, вкладки-фильтры, переключатели в строках, кнопки и меню «⋯», выбор строк, страницы и выбор видимых колонок. На телефоне строки превращаются в карточки: название, переключатель, значения парами и кнопки — без прокрутки вбок.
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_state | ui.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 — колонка скрыта, пока пользователь не включит её в меню «Колонки».
@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: её поиск и фильтры меняют параметры страницы, и страница перерисовывается.
@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 — миниатюра слева вместо иконки):
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 — панель справа (удобно для карточки записи поверх таблицы).
@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))Формы
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 проверяет обязательные поля, числа, суммы, сроки и выбор ещё до вызова действия и приводит значения к типам. Ошибку, которую видит только плагин, покажите у поля:
if price <= 0:
raise ValidationError({"price": "Цена должна быть больше нуля"})Не просите пользователя вводить данные по одному сообщению («введите название», «теперь цену»): одна форма — одно действие.
Выбор товаров
field.picker — выбор одного или нескольких объектов: товаров площадки плагина (товары GGsel, лоты FunPay или товары Playerok) и своих позиций. Два вида: поиск — поле, результаты появляются по мере ввода; каталог — сетка карточек с картинкой, названием, ценой и меткой. Выбранное показывается карточками над полем, их можно убрать или заменить. Работает в формах, мастерах и редакторе.
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.
@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 — панель справа (или окно) для создания и изменения одного объекта: связки, правила, товара. Поля, ошибки под полями, «Сохранить», «Отмена» и «Удалить» с подтверждением; после успешного сохранения панель закрывается, а страница обновляется.
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"встают в ряд.
@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 и их много.
@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 · Проверка». Над шагами — полоса прогресса из отрезков, пройденные шаги открываются нажатием, шаги сменяются плавно.
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=).
Сообщения покупателю
Для плагинов, которые пишут покупателям, есть редактор шаблона и предпросмотр в стиле чата площадки.
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("имя") ссылается на существующий файл.
python -m lovsel_sdk.testing my_plugin.pyfrom 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 объявлены.
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».
Примеры можно скачать кнопкой «Пример плагина» в документации, загрузить в «Плагины → Разработчикам» и открыть у себя — без публикации в магазине.