diff --git a/ui.js b/ui.js
new file mode 100644
index 0000000..7c365a2
--- /dev/null
+++ b/ui.js
@@ -0,0 +1,306 @@
+// ui_showcase.js — демонстрация UI Scriptable
+// Запускается через твой лоадер (__main(ctx))
+
+async function __main(ctx) {
+ while (true) {
+ const a = new Alert();
+ a.title = "UI Showcase";
+ a.message = "Выбери модуль для демонстрации";
+ a.addAction("1) Alert (кнопки/форма)");
+ a.addAction("2) UITable (список + скролл)");
+ a.addAction("3) WebView (HTML-UI, кнопки, инпуты)");
+ a.addAction("4) QuickLook + DrawContext");
+ a.addAction("5) Виджет (ListWidget, превью)");
+ a.addAction("6) Safari (встроенный)");
+ a.addAction("7) Локальное уведомление");
+ a.addCancelAction("Выход");
+ const i = await a.presentSheet();
+ if (i === -1) return;
+
+ try {
+ if (i === 0) await demoAlertForm();
+ else if (i === 1) await demoUITable();
+ else if (i === 2) await demoWebView();
+ else if (i === 3) await demoQuickLook();
+ else if (i === 4) { const end = await demoWidget(); if (end) return; }
+ else if (i === 5) await demoSafari();
+ else if (i === 6) await demoNotification(ctx);
+ } catch (e) {
+ await toast("Ошибка", String(e && e.message ? e.message : e));
+ }
+ }
+}
+
+/* 1) ALERT: кнопки + текстовые поля */
+async function demoAlertForm() {
+ const a = new Alert();
+ a.title = "Форма";
+ a.message = "Alert с полями ввода и кнопками";
+ a.addTextField("Имя", "");
+ a.addTextField("Email", "");
+ a.addAction("OK");
+ a.addDestructiveAction("Сбросить");
+ a.addCancelAction("Отмена");
+ const idx = await a.presentAlert();
+
+ const name = a.textFieldValue(0) || "";
+ const email = a.textFieldValue(1) || "";
+
+ if (idx === 0) {
+ const b = new Alert();
+ b.title = "Принято";
+ b.message = `Имя: ${name}\nEmail: ${email}`;
+ b.addAction("Скопировать");
+ b.addCancelAction("Закрыть");
+ if (await b.presentAlert() === 0) Pasteboard.copy(`${name} <${email}>`);
+ } else if (idx === 1) {
+ await toast("Сброшено", "Поля очищены");
+ }
+}
+
+/* 2) UITABLE: список, скролл, иконки, onSelect */
+async function demoUITable() {
+ const table = new UITable();
+ table.showSeparators = true;
+
+ // Заголовок
+ {
+ const r = new UITableRow();
+ const c = r.addText("История событий", new Date().toLocaleString());
+ c.titleFont = Font.boldSystemFont(16);
+ c.subtitleFont = Font.systemFont(12);
+ table.addRow(r);
+ }
+
+ // Генерируем 20 элементов
+ for (let i = 1; i <= 20; i++) {
+ const r = new UITableRow();
+ r.cellSpacing = 8;
+
+ // Иконка SF Symbol
+ try {
+ const sf = SFSymbol.named(i % 2 ? "barcode" : "qrcode");
+ sf.applyFont(Font.systemFont(18));
+ r.addImage(sf.image);
+ } catch { /* не критично */ }
+
+ const title = `Элемент #${i}`;
+ const subtitle = i % 2 ? "CODE_128 · 1234567890" : "QR_CODE · https://example.com";
+ r.addText(title, subtitle);
+
+ r.onSelect = async () => {
+ Pasteboard.copy(title + " — " + subtitle);
+ await toast("Скопировано", title);
+ };
+ table.addRow(r);
+ }
+
+ await table.present();
+}
+
+/* 3) WEBVIEW: кастомный HTML-экран + двусторонний обмен */
+async function demoWebView() {
+ const wv = new WebView();
+ let clicks = 0;
+
+ wv.shouldAllowRequest = req => {
+ const u = req.url || "";
+ if (u.startsWith("scriptable://action")) {
+ const q = qs(u);
+ const type = q.type || "";
+ if (type === "inc") { clicks++; wv.evaluateJavaScript(`setCount(${clicks})`, false); }
+ if (type === "save") {
+ const val = q.value || "";
+ Pasteboard.copy(val);
+ toast("Сохранено", "Строка скопирована в буфер");
+ }
+ return false; // не уходим со страницы
+ }
+ return true;
+ };
+
+ await wv.loadHTML(webviewHTML());
+ await wv.present(true);
+
+ // Пример вызова JS из Scriptable -> страница
+ await wv.evaluateJavaScript(`setCount(${clicks})`, false);
+
+ function webviewHTML() {
+ return `
+
+
WebView UI
+
+
+
+
+
+
Клик по кнопке отправляет событие обратно в Scriptable
+
+
+
+
Введи строку, затем «Сохранить»:
+
+
+
+
+
Строка уйдёт в Scriptable и скопируется в буфер
+
+
+
+
+
Скроллируемая область:
+
+
+
+
+
+`;
+ }
+}
+
+/* 4) QUICKLOOK + рисование через DrawContext */
+async function demoQuickLook() {
+ const img = renderCardImage({
+ title: "Scriptable UI",
+ subtitle: "QuickLook + DrawContext",
+ badge: new Date().toLocaleTimeString()
+ });
+ await QuickLook.present(img);
+
+ const a = new Alert();
+ a.title = "Сохранить картинку?";
+ a.addAction("Сохранить в Фото");
+ a.addCancelAction("Нет");
+ if (await a.presentAlert() === 0) Photos.save(img);
+}
+
+function renderCardImage({ title, subtitle, badge }) {
+ const W = 900, H = 500;
+ const ctx = new DrawContext();
+ ctx.size = new Size(W, H);
+ ctx.opaque = true;
+
+ // фон
+ const bg = new LinearGradient();
+ bg.colors = [new Color("#0b1220"), new Color("#111827")];
+ bg.locations = [0, 1];
+ ctx.setGradient(bg);
+ ctx.fillRect(new Rect(0, 0, W, H));
+
+ // карточка
+ const cardRect = new Rect(40, 40, W-80, H-80);
+ ctx.setFillColor(new Color("#0f1b2e"));
+ ctx.fillRoundedRect(cardRect, 26);
+
+ // заголовок
+ ctx.setTextColor(Color.white());
+ ctx.setFont(Font.boldSystemFont(48));
+ ctx.drawTextInRect(title, new Rect(70, 90, W-140, 60));
+
+ // подзаголовок
+ ctx.setTextColor(new Color("#c7d2fe"));
+ ctx.setFont(Font.systemFont(26));
+ ctx.drawTextInRect(subtitle, new Rect(70, 160, W-140, 40));
+
+ // бейдж
+ const badgeRect = new Rect(W-70-200, H-70-50, 200, 50);
+ ctx.setFillColor(new Color("#1d4ed8"));
+ ctx.fillRoundedRect(badgeRect, 12);
+ ctx.setTextColor(Color.white());
+ ctx.setFont(Font.boldSystemFont(22));
+ ctx.drawTextInRect(badge, new Rect(badgeRect.x, badgeRect.y+12, badgeRect.width, 30));
+
+ return ctx.getImage();
+}
+
+/* 5) ВИДЖЕТ: создаём ListWidget и показываем превью (завершает скрипт) */
+async function demoWidget() {
+ const warn = new Alert();
+ warn.title = "Показать превью виджета?";
+ warn.message = "Это завершит выполнение скрипта.\nПерезапусти лоадер, чтобы вернуться в меню.";
+ warn.addAction("Показать");
+ warn.addCancelAction("Отмена");
+ if (await warn.presentAlert() !== 0) return false;
+
+ const w = new ListWidget();
+ const grad = new LinearGradient();
+ grad.colors = [new Color("#0b1220"), new Color("#1f2937")];
+ grad.locations = [0, 1];
+ w.backgroundGradient = grad;
+
+ const t1 = w.addText("UI Showcase");
+ t1.font = Font.boldSystemFont(16);
+ t1.textColor = Color.white();
+
+ w.addSpacer(6);
+ const t2 = w.addText("Последний элемент: #7");
+ t2.textColor = new Color("#93c5fd");
+ t2.font = Font.systemFont(12);
+
+ w.addSpacer(12);
+ const t3 = w.addText(new Date().toLocaleString());
+ t3.textColor = Color.gray();
+ t3.font = Font.systemFont(10);
+
+ Script.setWidget(w);
+ Script.complete();
+ return true; // сказали выходим
+}
+
+/* 6) SAFARI: открываем встроенный браузер */
+async function demoSafari() {
+ await Safari.openInApp("https://example.com");
+}
+
+/* 7) NOTIFICATION: локальное уведомление */
+async function demoNotification(ctx) {
+ const n = new Notification();
+ n.title = "Scriptable";
+ n.body = "Это локальное уведомление";
+ await n.schedule();
+ await toast("Отправлено", "Проверь уведомления iOS");
+}
+
+/* Вспомогательные */
+async function toast(title, body) {
+ try {
+ const n = new Notification();
+ n.title = title; n.body = body;
+ // уведомление без звука как «тост»
+ n.sound = "none";
+ await n.schedule();
+ } catch {}
+}
+
+function qs(url) {
+ const out = {}; const i = url.indexOf("?");
+ if (i < 0) return out;
+ for (const p of url.slice(i+1).split("&")) {
+ const [k, v] = p.split("=");
+ out[decodeURIComponent(k||"")] = decodeURIComponent(v||"");
+ }
+ return out;
+}