From 4ce78f277e21c4507cc2b1f3d10a4eff972762e5 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Wed, 29 Apr 2026 11:10:12 +0300 Subject: [PATCH] feat: migrate to svelte initial --- .env.example | 3 +- Dockerfile | 1 + bot/app/web/frontend/src/App.svelte | 1300 ++ bot/app/web/frontend/src/PreviewBoard.svelte | 216 + .../src/lib/components/ui/button.svelte | 38 + .../src/lib/components/ui/card.svelte | 12 + .../src/lib/components/ui/dialog.svelte | 30 + .../src/lib/components/ui/input.svelte | 24 + bot/app/web/frontend/src/lib/utils.js | 6 + bot/app/web/frontend/src/main.js | 10 + .../web/frontend/src/preview/BackTitle.svelte | 17 + .../frontend/src/preview/PhoneFrame.svelte | 12 + .../src/preview/PreviewMethods.svelte | 26 + .../frontend/src/preview/PreviewNav.svelte | 11 + bot/app/web/frontend/src/styles.css | 1174 ++ bot/app/web/frontend/vite.config.mjs | 36 + bot/app/web/subscription_webapp.py | 1 + bot/app/web/templates/subscription_webapp.css | 1177 +- .../web/templates/subscription_webapp.html | 283 +- bot/app/web/templates/subscription_webapp.js | 16797 +++++++++++++--- config/settings.py | 3 + package-lock.json | 994 +- package.json | 15 +- 23 files changed, 19245 insertions(+), 2941 deletions(-) create mode 100644 bot/app/web/frontend/src/App.svelte create mode 100644 bot/app/web/frontend/src/PreviewBoard.svelte create mode 100644 bot/app/web/frontend/src/lib/components/ui/button.svelte create mode 100644 bot/app/web/frontend/src/lib/components/ui/card.svelte create mode 100644 bot/app/web/frontend/src/lib/components/ui/dialog.svelte create mode 100644 bot/app/web/frontend/src/lib/components/ui/input.svelte create mode 100644 bot/app/web/frontend/src/lib/utils.js create mode 100644 bot/app/web/frontend/src/main.js create mode 100644 bot/app/web/frontend/src/preview/BackTitle.svelte create mode 100644 bot/app/web/frontend/src/preview/PhoneFrame.svelte create mode 100644 bot/app/web/frontend/src/preview/PreviewMethods.svelte create mode 100644 bot/app/web/frontend/src/preview/PreviewNav.svelte create mode 100644 bot/app/web/frontend/src/styles.css create mode 100644 bot/app/web/frontend/vite.config.mjs diff --git a/.env.example b/.env.example index cfbd28e..c337e4e 100644 --- a/.env.example +++ b/.env.example @@ -39,7 +39,8 @@ WEBAPP_SERVER_HOST=0.0.0.0 # WEBAPP_SERVER_PORT=8081 # Internal/published Mini App port WEBAPP_TITLE="/minishop" # Mini App title WEBAPP_PRIMARY_COLOR="#00fe7a" # Main UI color -WEBAPP_LOGO_URL=https://em-content.zobj.net/source/animated-noto-color-emoji/427/dotted-line-face_1fae5.gif # Optional logo URL; shown in the header and login screen, leave empty to hide +WEBAPP_LOGO_URL= # Optional logo URL; if empty the emoji below is used +WEBAPP_LOGO_EMOJI="🫥" # Emoji logo fallback shown in the header and login screen WEBAPP_SESSION_SECRET= # Optional: HMAC secret for webapp sessions; generated if empty WEBHOOK_SECRET_TOKEN= # Optional: Telegram webhook secret token; generated if empty WEBAPP_SESSION_TTL_SECONDS=86400 # Web App session lifetime (24h) diff --git a/Dockerfile b/Dockerfile index 84176c4..c664076 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ COPY package.json package-lock.json* ./ RUN --mount=type=cache,target=/root/.npm \ if [ -f package-lock.json ]; then npm ci; else npm install; fi +COPY bot/app/web/frontend ./bot/app/web/frontend COPY bot/app/web/templates ./bot/app/web/templates COPY scripts/build_subscription_webapp_js.mjs ./scripts/build_subscription_webapp_js.mjs diff --git a/bot/app/web/frontend/src/App.svelte b/bot/app/web/frontend/src/App.svelte new file mode 100644 index 0000000..de703a2 --- /dev/null +++ b/bot/app/web/frontend/src/App.svelte @@ -0,0 +1,1300 @@ + + + + {brandTitle} + + +{#if isPreviewBoard} + +{:else} +
+ {#if mode === "loading"} +
+
+ {#if CFG.logoUrl} + + {:else} + {brandEmoji} + {/if} +
+
Загрузка...
+
+ {:else if mode === "login"} +
+ {#if screen === "code"} +
+ +
+

Подтверждение по email

+

Мы отправили код на {pendingEmail}

+
+ +
+
+ + + {#if authStatus} +
{authStatus}
+ {/if} + +
+ {:else} +
+ + +
+ + +
+ {#if authMode === "email"} +
+
Вход по email
+ +
+ {:else} +
+
Вход через Telegram
+ {#if tg?.initData} + + {:else} +
+ {/if} +
+ {/if} + {#if authStatus} +
{authStatus}
+ {/if} +
+
+ Нет аккаунта? Создать +
+
+ {/if} +
+ {:else} +
+ {#if screen === "invite" || screen === "settings"} +
+
+
+ {#if CFG.logoUrl} + + {:else} + {brandEmoji} + {/if} +
+ {brandTitle} +
+
+ {/if} + + {#if screen === "home"} +
+ + +
+ +
+ +
+

{subscription.active ? "Подписка активна" : "Подписка не активна"}

+

{subscription.end_date_text ? `до ${subscription.end_date_text}` : subscription.remaining_text}

+
+
+
+ + +
+ Использовано трафика + {trafficLabel(subscription)} +
+
+ +
+
{trafficPercent(subscription)}%
+
+ +
+ + + +
+
+
+ {:else if screen === "tariff-select"} +
+
+ +
+

Выбор тарифа

+

Экраны тарифов пока сверстаны без подключения

+
+ +
+
+ {#each demoTariffs as tariff} + + {/each} +
+ +
+ {:else if screen === "payment"} +
+
+ +
+

Подписка

+

Выберите срок подписки

+
+ +
+
+ {#each plans as plan} + + {/each} +
+ + Итого
К оплате
+ {selectedPlan ? formatMoney(selectedPlan.price, selectedPlan.currency) : "..."} +
+
+ {#if methods.length} + {#each methods as method} + {@const meta = methodMeta(method)} + + {/each} + {:else} + Способы оплаты пока не настроены + {/if} +
+ +
+ {:else if screen === "traffic-payment"} +
+
+ +
+

Трафик

+

Выберите пакет трафика

+
+ +
+
+ {#each trafficPackages as pack} + + {/each} +
+ + Итого
К оплате
+ {formatMoney(selectedTrafficPackage.price)} +
+
+ {#each DEV_MOCK.data.payment_methods as method} + {@const meta = methodMeta(method)} + + {/each} +
+ +
+ {:else if screen === "change-tariff"} +
+
+ +
+

Смена тарифа

+

Остаток 12 дней будет пересчитан

+
+ +
+
+ {#each changeTariffs as tariff} + + {/each} +
+ +
+ {:else if screen === "invite"} +
+ +
Ваша реферальная ссылка
+
+ {referral.webapp_link || referral.bot_link || "Ссылка пока недоступна"} + +
+
+ + +
+ Ваш бонус + +7 дней за каждого друга +

Друг получит +3 дня к подписке после регистрации.

+
+
+ +
Промокод
+
+ + +
+ {#if promoStatus} +

{promoStatus}

+ {/if} +
+
+ {:else if screen === "settings"} +
+ +
+ {#if profileAvatarUrl} + Аватар пользователя + {:else} + + {/if} +
+
+ {telegramProfileName} + {profileEmail} + {profileTelegramId} +
+
+
+ + + + +
+
+ {/if} + + {#if screen === "home" || screen === "invite" || screen === "settings"} + + {/if} +
+ {/if} + + (confirmTariffOpen = false)} + > +
+ + +
+
+ + {#if toastText} +
{toastText}
+ {/if} +
+{/if} diff --git a/bot/app/web/frontend/src/PreviewBoard.svelte b/bot/app/web/frontend/src/PreviewBoard.svelte new file mode 100644 index 0000000..09cfabf --- /dev/null +++ b/bot/app/web/frontend/src/PreviewBoard.svelte @@ -0,0 +1,216 @@ + + +
+ +
+ +
+ +
+ +

Подписка активна

до {sub.end_date_text}

+
+
+ +
Использовано трафика{sub.traffic_used} из {sub.traffic_limit}
+
+
18%
+
+
+ + + +
+
+ +
+
+ + +
{logoEmoji}
{title}
+
+ {#each tariffs as tariff, index} +
+ + {tariff[1]}{tariff[2]}{tariff[3]} + {#if index === 0}{:else}{/if} +
+ {/each} +
+ +
+ + + +
+ {#each plans as plan, index} +
+ {plan.title}{money(plan.price)}{money(Math.round(plan.price / plan.months))}/мес + {#if index === 1}{/if} +
+ {/each} +
+ Итого
К оплате
790 ₽
+ + +
+ + + +
+ {#each traffic as pack, index} +
+ {pack[0]} ГБ{money(pack[1])}{money(Math.round(pack[1] / pack[0]))}/ГБ + {#if index === 2}{/if} +
+ {/each} +
+ Итого
К оплате
990 ₽
+ + +
+ + + +
+
ПодпискаБезлимитный трафикДоплата 190 ₽
+
ТрафикПакеты гигабайтДоплата не требуется
+
ПремиумМаксимальная скоростьДоплата 390 ₽
+
+ +
+ + Сменить тариф без доплаты? +

Остаток 12 дней будет пересчитан по новому тарифу.

+ + +
+
+ + +
{logoEmoji}
{title}
+ +
Ваша реферальная ссылка
+
https://minishop.app/ref/ABCD1234
+
+ +
Ваш бонус+7 дней за каждого друга

Друг получит +3 дня к подписке.

+
+ +
+ + +
{logoEmoji}
{title}
+ +
+ {#if previewAvatar} + Аватар пользователя + {:else} + + {/if} +
+
+ {previewTelegramName} + {previewEmail} + {previewTelegramId} +
+
+ {#each settingsRows as row} +
+ + {row[1]}{row[2]} + +
+ {/each} +
+
+ + + + +
Вход по email
+ +
или
+ +
+
Нет аккаунта? Создать
+
+ + + +
{#each [1, 2, 3, 4, 5, 6] as digit}{digit}{/each}
+ + +
+
+ + + diff --git a/bot/app/web/frontend/src/lib/components/ui/button.svelte b/bot/app/web/frontend/src/lib/components/ui/button.svelte new file mode 100644 index 0000000..aef6d9a --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/ui/button.svelte @@ -0,0 +1,38 @@ + + +{#if href} + + + +{:else} + +{/if} diff --git a/bot/app/web/frontend/src/lib/components/ui/card.svelte b/bot/app/web/frontend/src/lib/components/ui/card.svelte new file mode 100644 index 0000000..3156ce8 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/ui/card.svelte @@ -0,0 +1,12 @@ + + +
+ +
diff --git a/bot/app/web/frontend/src/lib/components/ui/dialog.svelte b/bot/app/web/frontend/src/lib/components/ui/dialog.svelte new file mode 100644 index 0000000..9b34e81 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/ui/dialog.svelte @@ -0,0 +1,30 @@ + + +{#if open} + +{/if} diff --git a/bot/app/web/frontend/src/lib/components/ui/input.svelte b/bot/app/web/frontend/src/lib/components/ui/input.svelte new file mode 100644 index 0000000..9b7d187 --- /dev/null +++ b/bot/app/web/frontend/src/lib/components/ui/input.svelte @@ -0,0 +1,24 @@ + + + diff --git a/bot/app/web/frontend/src/lib/utils.js b/bot/app/web/frontend/src/lib/utils.js new file mode 100644 index 0000000..378ccef --- /dev/null +++ b/bot/app/web/frontend/src/lib/utils.js @@ -0,0 +1,6 @@ +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs) { + return twMerge(clsx(inputs)); +} diff --git a/bot/app/web/frontend/src/main.js b/bot/app/web/frontend/src/main.js new file mode 100644 index 0000000..4eb14c2 --- /dev/null +++ b/bot/app/web/frontend/src/main.js @@ -0,0 +1,10 @@ +import { mount } from "svelte"; + +import App from "./App.svelte"; +import "./styles.css"; + +const target = document.getElementById("app"); + +if (target) { + mount(App, { target }); +} diff --git a/bot/app/web/frontend/src/preview/BackTitle.svelte b/bot/app/web/frontend/src/preview/BackTitle.svelte new file mode 100644 index 0000000..b7ade24 --- /dev/null +++ b/bot/app/web/frontend/src/preview/BackTitle.svelte @@ -0,0 +1,17 @@ + + +
+ +
+

{title}

+

{subtitle}

+
+ +
diff --git a/bot/app/web/frontend/src/preview/PhoneFrame.svelte b/bot/app/web/frontend/src/preview/PhoneFrame.svelte new file mode 100644 index 0000000..f6340de --- /dev/null +++ b/bot/app/web/frontend/src/preview/PhoneFrame.svelte @@ -0,0 +1,12 @@ + + +
+

{number}. {label}

+
+ +
+
diff --git a/bot/app/web/frontend/src/preview/PreviewMethods.svelte b/bot/app/web/frontend/src/preview/PreviewMethods.svelte new file mode 100644 index 0000000..b2db0d7 --- /dev/null +++ b/bot/app/web/frontend/src/preview/PreviewMethods.svelte @@ -0,0 +1,26 @@ + + +
+ {#each methods as method, index} +
+ + + {method.name} + {note(index)} + +
+ {/each} +
diff --git a/bot/app/web/frontend/src/preview/PreviewNav.svelte b/bot/app/web/frontend/src/preview/PreviewNav.svelte new file mode 100644 index 0000000..003b881 --- /dev/null +++ b/bot/app/web/frontend/src/preview/PreviewNav.svelte @@ -0,0 +1,11 @@ + + + diff --git a/bot/app/web/frontend/src/styles.css b/bot/app/web/frontend/src/styles.css new file mode 100644 index 0000000..4d5c91c --- /dev/null +++ b/bot/app/web/frontend/src/styles.css @@ -0,0 +1,1174 @@ +:root { + color-scheme: dark; + --accent: #00fe7a; + --bg: #03070b; + --panel: #111820; + --panel-2: #0b1118; + --panel-3: #17212b; + --border: rgba(255, 255, 255, 0.12); + --border-strong: rgba(255, 255, 255, 0.2); + --text: #f2f7f4; + --muted: #a9b4b0; + --dim: #68736f; + --danger: #ff6b6b; + --blue: #2d9cff; + --radius: 8px; + font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + min-height: 100%; + margin: 0; +} + +body { + background: #02070b; + color: var(--text); + -webkit-font-smoothing: antialiased; + letter-spacing: 0; +} + +button, +input { + font: inherit; + letter-spacing: 0; +} + +button { + cursor: pointer; +} + +button:disabled { + cursor: progress; + opacity: 0.58; +} + +a { + color: inherit; +} + +.app-shell { + min-height: 100dvh; + background: linear-gradient(180deg, #06100c 0%, #03070b 26%, #02060a 100%); +} + +.phone-screen { + position: relative; + width: min(100vw, 440px); + min-height: 100dvh; + margin: 0 auto; + overflow-x: hidden; + padding: max(16px, env(safe-area-inset-top)) 14px max(18px, env(safe-area-inset-bottom)); +} + +.content { + display: grid; + gap: 10px; +} + +.content.with-nav { + padding-bottom: 86px; +} + +.loader { + display: grid; + min-height: 100dvh; + place-items: center; + align-content: center; + gap: 14px; + color: var(--muted); + font-weight: 800; +} + +.app-header, +.preview-header, +.screen-head { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 12px; + min-height: 48px; +} + +.screen-head { + grid-template-columns: 42px minmax(0, 1fr) 42px; + margin-bottom: 6px; +} + +.center-title { + align-items: start; +} + +.center-copy { + min-width: 0; + text-align: center; +} + +.screen-head h1, +.center-copy h1 { + margin: 0; + color: var(--text); + font-size: 18px; + line-height: 1.16; + font-weight: 850; +} + +.screen-head p, +.center-copy p { + margin: 3px 0 0; + color: var(--muted); + font-size: 12px; + line-height: 1.3; +} + +.screen-head strong { + color: var(--accent); +} + +.brand-row { + display: flex; + min-width: 0; + align-items: center; + gap: 10px; +} + +.brand-row strong { + min-width: 0; + overflow: hidden; + color: var(--text); + font-size: 15px; + font-weight: 850; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accent-title .brand-row strong { + color: var(--accent); +} + +.brand-mark { + display: grid; + width: 38px; + height: 38px; + flex: 0 0 auto; + place-items: center; + color: var(--accent); + font-size: 26px; + line-height: 1; +} + +.brand-mark img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.brand-mark-lg { + width: 66px; + height: 66px; + font-size: 46px; +} + +.brand-mark-xl { + width: 96px; + height: 96px; + font-size: 70px; +} + +.card { + position: relative; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius); + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)), + var(--panel); + box-shadow: + 0 18px 48px rgba(0, 0, 0, 0.22), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + padding: 14px; +} + +.card-active { + border-color: color-mix(in srgb, var(--accent) 76%, var(--border)); + box-shadow: + 0 18px 48px rgba(0, 0, 0, 0.22), + 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.card-compact { + padding: 11px; +} + +.status-card { + padding: 16px; +} + +.sub-status { + display: flex; + align-items: center; + gap: 12px; + color: var(--accent); +} + +.sub-status h2 { + margin: 0; + font-size: 15px; + line-height: 1.16; +} + +.sub-status p { + margin: 5px 0 0; + color: var(--text); + font-size: 12px; + opacity: 0.82; +} + +.traffic-top, +.total-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.traffic-top span, +.card-label, +.total-card span, +.settings-row small, +.select-card small, +.method-card small { + color: var(--muted); + font-size: 12px; +} + +.traffic-top strong { + font-size: 12px; + font-weight: 850; +} + +.progress { + position: relative; + height: 8px; + margin-top: 12px; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); +} + +.progress span { + position: absolute; + inset: 0 auto 0 0; + width: 0; + border-radius: inherit; + background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 72%, white)); + box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 40%, transparent); +} + +.traffic-percent { + margin-top: 6px; + text-align: right; + color: var(--muted); + font-size: 11px; +} + +.action-stack { + display: grid; + gap: 8px; +} + +.btn { + display: inline-flex; + min-height: 44px; + min-width: 0; + align-items: center; + justify-content: center; + gap: 8px; + border: 1px solid transparent; + border-radius: var(--radius); + padding: 10px 14px; + text-align: center; + text-decoration: none; + font-size: 14px; + font-weight: 850; + line-height: 1.1; + transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, color 0.16s ease; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn-primary { + border-color: color-mix(in srgb, var(--accent) 72%, transparent); + background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 94%, white), var(--accent)); + color: #031009; + box-shadow: 0 14px 34px color-mix(in srgb, var(--accent) 20%, transparent); +} + +.btn-secondary, +.btn-outline { + border-color: var(--border-strong); + background: rgba(255, 255, 255, 0.045); + color: var(--text); +} + +.btn-outline { + border-color: color-mix(in srgb, var(--accent) 60%, var(--border)); + color: var(--accent); +} + +.btn-ghost { + background: transparent; + color: var(--muted); +} + +.btn-telegram { + border-color: #2d9cff; + background: linear-gradient(180deg, #2f9ff4, #1786df); + color: white; +} + +.btn-icon { + border-color: var(--border); + background: rgba(255, 255, 255, 0.04); + color: var(--text); +} + +.btn-square { + width: 42px; + min-width: 42px; + height: 42px; + padding: 0; +} + +.btn-sm { + min-height: 36px; + padding: 8px 10px; + font-size: 12px; +} + +.wide { + width: 100%; +} + +.input { + width: 100%; + min-height: 44px; + min-width: 0; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(4, 8, 11, 0.48); + color: var(--text); + outline: none; + padding: 11px 12px; + font-size: 13px; + font-weight: 700; +} + +.input:focus { + border-color: color-mix(in srgb, var(--accent) 68%, var(--border)); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent); +} + +.input.muted { + display: flex; + align-items: center; + color: var(--dim); +} + +.bottom-action { + margin-top: 4px; +} + +.period-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; +} + +.period-card, +.method-card, +.select-card, +.settings-row { + border: 1px solid var(--border); + border-radius: var(--radius); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.018)); + color: var(--text); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045); +} + +.period-card { + position: relative; + display: grid; + min-height: 82px; + place-items: center; + gap: 4px; + padding: 10px 8px; +} + +.period-card strong { + font-size: 12px; + white-space: nowrap; +} + +.period-card span { + font-size: 16px; + font-weight: 850; +} + +.period-card small { + color: var(--muted); + font-size: 10px; +} + +.period-card svg { + position: absolute; + top: 8px; + right: 8px; + color: var(--accent); +} + +.period-card.active, +.method-card.active, +.select-card.active { + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 9%, rgba(255, 255, 255, 0.035)); + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--accent) 50%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.06); +} + +.total-card { + padding: 12px 14px; +} + +.total-card strong { + font-size: 22px; + font-weight: 900; +} + +.method-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 7px; +} + +.method-card { + display: flex; + min-height: 48px; + align-items: center; + gap: 8px; + padding: 8px; + text-align: left; +} + +.method-card svg { + flex: 0 0 auto; +} + +.method-card span { + display: grid; + min-width: 0; + gap: 2px; +} + +.method-card strong, +.method-card small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.method-card strong { + font-size: 10px; +} + +.method-card small { + font-size: 8px; +} + +.tariff-list, +.settings-list { + display: grid; + gap: 8px; +} + +.select-card { + display: grid; + grid-template-columns: 52px minmax(0, 1fr) 24px; + align-items: center; + gap: 12px; + padding: 12px; + text-align: left; +} + +.tariff-list.compact .select-card { + grid-template-columns: minmax(0, 1fr) auto 24px; +} + +.select-icon { + display: grid; + width: 42px; + height: 42px; + place-items: center; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--accent); +} + +.select-card span:not(.select-icon) { + display: grid; + gap: 3px; + min-width: 0; +} + +.select-card strong { + font-size: 14px; +} + +.select-card em { + color: var(--muted); + font-size: 11px; + font-style: normal; +} + +.copy-row, +.email-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; +} + +.copy-row code { + min-width: 0; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(0, 0, 0, 0.2); + color: var(--text); + padding: 12px; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bonus-card { + display: flex; + align-items: center; + gap: 14px; +} + +.bonus-card > svg { + flex: 0 0 auto; + color: var(--accent); +} + +.bonus-card span { + color: var(--muted); + font-size: 12px; +} + +.bonus-card strong { + display: block; + color: var(--accent); + font-size: 17px; +} + +.bonus-card p { + margin: 5px 0 0; + color: var(--muted); + font-size: 12px; +} + +.settings-profile { + display: flex; + align-items: center; + gap: 12px; +} + +.settings-avatar { + display: grid; + width: 58px; + height: 58px; + flex: 0 0 auto; + place-items: center; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 999px; + background: rgba(255, 255, 255, 0.03); +} + +.settings-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.settings-avatar svg { + color: var(--accent); +} + +.settings-profile-meta { + display: grid; + min-width: 0; + gap: 3px; +} + +.settings-profile-meta strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; +} + +.settings-profile-meta small { + overflow: hidden; + color: var(--muted); + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; +} + +.settings-row { + display: grid; + grid-template-columns: 28px minmax(0, 1fr) 18px; + align-items: center; + gap: 10px; + min-height: 50px; + padding: 9px 10px; + text-align: left; +} + +.settings-row > svg:first-child { + color: var(--text); + opacity: 0.9; +} + +.settings-row > svg:last-child { + color: var(--muted); +} + +.settings-row span { + display: grid; + min-width: 0; + gap: 2px; +} + +.settings-row strong, +.settings-row small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-row strong { + font-size: 12px; +} + +.bottom-nav { + position: absolute; + right: 14px; + bottom: max(10px, env(safe-area-inset-bottom)); + left: 14px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2px; + min-height: 64px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(7, 12, 17, 0.88); + box-shadow: 0 16px 38px rgba(0, 0, 0, 0.36); + backdrop-filter: blur(16px); +} + +.bottom-nav.static { + position: absolute; +} + +.bottom-nav button { + display: grid; + place-items: center; + align-content: center; + gap: 4px; + border: 0; + background: transparent; + color: var(--muted); + font-size: 10px; + font-weight: 800; +} + +.bottom-nav button.active { + color: var(--accent); +} + +.home-layout { + display: flex; + min-height: calc(100dvh - 34px); + flex-direction: column; + gap: 18px; + padding-bottom: 86px; +} + +.home-brand { + padding-top: clamp(26px, 10dvh, 74px); +} + +.home-bottom { + display: grid; + gap: 10px; + margin-top: auto; +} + +.auth-screen { + display: grid; + align-content: center; +} + +.auth-card-wrap { + display: grid; + gap: 16px; +} + +.login-brand { + display: grid; + justify-items: center; + gap: 10px; + text-align: center; +} + +.login-brand.small { + gap: 5px; +} + +.login-brand h1 { + margin: 0; + overflow-wrap: anywhere; + color: var(--accent); + font-size: 32px; + font-weight: 900; + line-height: 1.04; +} + +.login-brand p { + margin: 0; + color: var(--muted); + font-size: 13px; +} + +.auth-card { + display: grid; + gap: 14px; +} + +.segmented { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(0, 0, 0, 0.18); + padding: 4px; +} + +.segmented button { + min-height: 35px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--muted); + font-weight: 850; +} + +.segmented button.active { + background: color-mix(in srgb, var(--accent) 11%, transparent); + color: var(--accent); +} + +.auth-pane { + display: grid; + gap: 10px; +} + +.auth-pane .field-label, +.auth-card .field-label { + color: var(--text); + font-size: 13px; + font-weight: 850; +} + +.telegram-widget { + display: grid; + min-height: 54px; + place-items: center; +} + +.auth-bottom { + text-align: center; + color: var(--muted); + font-size: 13px; +} + +.auth-bottom strong { + color: var(--accent); +} + +.or-line { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 10px; + color: var(--muted); + font-size: 12px; +} + +.or-line span { + height: 1px; + background: var(--border); +} + +.otp-wrap { + display: grid; + align-content: center; + gap: 20px; + min-height: calc(100dvh - 120px); +} + +.otp-input-wrap { + position: relative; + display: block; +} + +.otp-input-wrap input { + position: absolute; + inset: 0; + z-index: 2; + width: 100%; + border: 0; + background: transparent; + color: transparent; + caret-color: transparent; + outline: none; +} + +.otp-slots { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 10px; +} + +.otp-slots span { + display: grid; + aspect-ratio: 1; + place-items: center; + border: 1px solid color-mix(in srgb, var(--accent) 74%, var(--border)); + border-radius: var(--radius); + background: rgba(255, 255, 255, 0.035); + font-size: 25px; + font-weight: 900; +} + +.otp-slots span.filled { + background: color-mix(in srgb, var(--accent) 9%, rgba(255, 255, 255, 0.04)); +} + +.link-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border: 0; + background: transparent; + color: var(--muted); + font-size: 13px; +} + +.status-line { + color: var(--accent); + font-size: 12px; + line-height: 1.4; +} + +.status-line.error, +.error { + color: var(--danger); +} + +.empty-card { + grid-column: 1 / -1; + color: var(--muted); + font-size: 13px; +} + +.dialog { + position: fixed; + inset: 0; + z-index: 40; + display: grid; + place-items: center; + padding: 16px; +} + +.dialog-backdrop { + position: absolute; + inset: 0; + border: 0; + background: rgba(0, 0, 0, 0.68); + backdrop-filter: blur(10px); +} + +.dialog-card { + position: relative; + z-index: 1; + display: grid; + width: min(100%, 360px); + gap: 16px; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--panel); + padding: 18px; + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.48); +} + +.dialog-head { + display: grid; + grid-template-columns: minmax(0, 1fr) 42px; + align-items: start; + gap: 12px; +} + +.dialog-head h2 { + margin: 0; + font-size: 17px; +} + +.dialog-head p { + margin: 6px 0 0; + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.dialog-actions { + display: grid; + gap: 8px; +} + +.toast { + position: fixed; + right: 16px; + bottom: 16px; + z-index: 60; + max-width: min(420px, calc(100vw - 32px)); + border: 1px solid color-mix(in srgb, var(--accent) 46%, var(--border)); + border-radius: var(--radius); + background: rgba(15, 21, 27, 0.92); + box-shadow: 0 16px 42px rgba(0, 0, 0, 0.42); + color: var(--text); + padding: 12px 14px; + font-size: 13px; + font-weight: 800; +} + +.preview-board { + display: grid; + width: max-content; + min-height: 100vh; + grid-template-columns: repeat(3, auto); + gap: 22px 38px; + align-items: start; + padding: 18px 74px 28px; + background: linear-gradient(180deg, #06100c 0%, #02060b 32%, #010407 100%); +} + +.preview-phone-wrap { + display: grid; + gap: 9px; +} + +.preview-phone-wrap h2 { + margin: 0; + text-align: center; + font-size: 18px; + line-height: 1.1; +} + +.preview-phone-wrap h2 span { + color: var(--accent); +} + +.preview-phone { + position: relative; + display: grid; + width: 430px; + min-height: 318px; + align-content: start; + gap: 8px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.22); + border-radius: 18px; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.015)), + #071017; + box-shadow: + 0 28px 80px rgba(0, 0, 0, 0.54), + inset 0 1px 0 rgba(255, 255, 255, 0.08); + padding: 15px 16px 16px; +} + +.preview-phone-wrap.wide .preview-phone { + width: 510px; +} + +.preview-phone .bottom-nav { + right: 16px; + bottom: 8px; + left: 16px; + min-height: 50px; + box-shadow: none; +} + +.preview-phone .bottom-nav span { + font-size: 9px; +} + +.preview-phone .card { + padding: 12px; +} + +.preview-phone .status-card { + padding: 14px; +} + +.preview-phone .btn { + min-height: 38px; + font-size: 12px; + padding: 8px 10px; +} + +.preview-phone .btn-square { + width: 36px; + min-width: 36px; + height: 36px; + padding: 0; +} + +.preview-phone .period-card { + min-height: 72px; +} + +.preview-phone .method-card { + min-height: 42px; +} + +.preview-phone .settings-row { + min-height: 39px; +} + +.preview-phone .settings-avatar { + width: 46px; + height: 46px; +} + +.preview-phone .settings-profile-meta strong { + font-size: 12px; +} + +.preview-phone .settings-profile-meta small { + font-size: 10px; +} + +.preview-phone .brand-mark { + width: 34px; + height: 34px; + font-size: 23px; +} + +.preview-phone .brand-mark-lg { + width: 56px; + height: 56px; + font-size: 39px; +} + +.preview-phone .brand-mark-xl { + width: 78px; + height: 78px; + font-size: 56px; +} + +.preview-phone .home-layout { + min-height: 300px; + gap: 12px; + padding-bottom: 58px; +} + +.preview-phone .home-brand { + padding-top: 0; +} + +.preview-phone .login-brand h1 { + font-size: 24px; +} + +.preview-modal { + position: absolute; + right: 18px; + bottom: 18px; + display: grid; + width: 178px; + gap: 8px; + justify-items: center; + border: 1px solid var(--border-strong); + border-radius: 14px; + background: rgba(20, 29, 35, 0.94); + box-shadow: 0 18px 52px rgba(0, 0, 0, 0.46); + padding: 16px; + text-align: center; +} + +.preview-modal svg { + color: var(--accent); +} + +.preview-modal strong { + font-size: 13px; +} + +.preview-modal p { + margin: 0; + color: var(--muted); + font-size: 10px; + line-height: 1.35; +} + +@media (max-width: 460px) { + .phone-screen { + width: 100vw; + padding-right: 10px; + padding-left: 10px; + } + + .period-grid, + .method-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .copy-row, + .email-row { + grid-template-columns: 1fr; + } +} + +@supports not (color: color-mix(in srgb, #000 50%, #fff)) { + .btn-primary { + background: var(--accent); + } +} diff --git a/bot/app/web/frontend/vite.config.mjs b/bot/app/web/frontend/vite.config.mjs new file mode 100644 index 0000000..a3d8952 --- /dev/null +++ b/bot/app/web/frontend/vite.config.mjs @@ -0,0 +1,36 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { defineConfig } from "vite"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const templateDir = path.resolve(__dirname, "../templates"); + +export default defineConfig({ + plugins: [svelte()], + build: { + outDir: templateDir, + emptyOutDir: false, + minify: false, + sourcemap: false, + cssCodeSplit: false, + lib: { + entry: path.resolve(__dirname, "src/main.js"), + name: "SubscriptionWebApp", + formats: ["iife"], + fileName: () => "subscription_webapp.js", + cssFileName: "subscription_webapp", + }, + rollupOptions: { + output: { + assetFileNames: (assetInfo) => { + if (assetInfo.name && assetInfo.name.endsWith(".css")) { + return "subscription_webapp.css"; + } + return "subscription_webapp.[name][extname]"; + }, + }, + }, + }, +}); diff --git a/bot/app/web/subscription_webapp.py b/bot/app/web/subscription_webapp.py index d689b16..4305b68 100644 --- a/bot/app/web/subscription_webapp.py +++ b/bot/app/web/subscription_webapp.py @@ -489,6 +489,7 @@ async def index_route(request: web.Request) -> web.Response: "title": settings.WEBAPP_TITLE, "primaryColor": settings.WEBAPP_PRIMARY_COLOR, "logoUrl": cached["logo_url"], + "logoEmoji": settings.WEBAPP_LOGO_EMOJI, "apiBase": "/api", "telegramLoginBotUsername": request.app.get("bot_username") or "", "supportUrl": cached["support_url"], diff --git a/bot/app/web/templates/subscription_webapp.css b/bot/app/web/templates/subscription_webapp.css index 656217f..16add18 100644 --- a/bot/app/web/templates/subscription_webapp.css +++ b/bot/app/web/templates/subscription_webapp.css @@ -1,2 +1,1175 @@ -/*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-bold:700;--font-weight-extrabold:800;--tracking-wide:.025em;--leading-tight:1.25;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html,body{margin:calc(var(--spacing) * 0);--tw-tracking:0;letter-spacing:0;width:100%;min-height:100%;overflow-x:hidden}body{background-color:var(--bg-primary);font-family:var(--font-sans);color:var(--text-primary);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-tap-highlight-color:transparent;overflow-x:hidden}body.modal-open{overflow:hidden}button,a{font:inherit}button{cursor:pointer;border-style:var(--tw-border-style);border-width:0}button:disabled{cursor:progress;opacity:.58}}@layer components{.btn-sm{min-height:38px;min-width:calc(var(--spacing) * 0);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding-inline:11px;padding-block:calc(var(--spacing) * 2);text-align:center;--tw-leading:1;--tw-font-weight:var(--font-weight-extrabold);font-size:13px;line-height:1;font-weight:var(--font-weight-extrabold);color:var(--text-secondary);transition-property:transform,border-color,background,color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.4,0,.2,1);background-color:#ffffff08;text-decoration-line:none;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-flex}@media (hover:hover){.btn-sm:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.btn-sm:hover{border-color:color-mix(in srgb,var(--accent) 42%,var(--border))}}.btn-sm:hover{background-color:var(--bg-card-hover)}}.metrics{gap:calc(var(--spacing) * 0);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff05;display:grid;overflow:hidden}.metric{min-height:calc(var(--spacing) * 12);align-items:center;gap:calc(var(--spacing) * 2.5);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);background-color:#ffffff05;grid-template-columns:minmax(0,.88fr) minmax(0,1.12fr);display:grid}.auth-tab{min-height:calc(var(--spacing) * 9);border-radius:var(--radius-sm);--tw-font-weight:850;color:var(--text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#0000;font-weight:850}.login-layout{justify-content:center;align-items:center;gap:calc(var(--spacing) * 4);width:min(100vw,560px);min-height:100vh;padding-inline:14px;padding-top:calc(max(var(--app-safe-top), 14px) + var(--app-safe-top-extra));padding-bottom:max(var(--app-safe-bottom), 18px);flex-direction:column;margin-inline:auto;display:none}.section-label{font-family:var(--font-mono);--tw-font-weight:var(--font-weight-extrabold);font-size:11px;font-weight:var(--font-weight-extrabold);color:var(--text-muted)}.app-loader{min-height:78vh;font-family:var(--font-mono);--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);color:var(--text-muted);place-items:center;display:grid}.auth-tabs-wrapper{gap:calc(var(--spacing) * 1);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);min-height:46px;padding:calc(var(--spacing) * 1);background-color:#05080d61;grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.panel-head{justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 3);display:flex}.section-title{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-leading:var(--leading-tight);line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);color:var(--text-primary)}.section-caption{color:var(--text-secondary);margin-top:3px;font-size:13px}.metric-label{min-width:calc(var(--spacing) * 0);font-family:var(--font-mono);--tw-font-weight:var(--font-weight-bold);font-size:11px;font-weight:var(--font-weight-bold);color:var(--text-muted)}.metric-value{min-width:calc(var(--spacing) * 0);text-align:right;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--text-primary)}.referral-link-row{align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);min-height:58px;padding-inline:calc(var(--spacing) * 3);background-color:#ffffff05;grid-template-columns:minmax(0,1fr) 48px;padding-block:11px;display:grid}.referral-link-value{margin-top:calc(var(--spacing) * 1);font-family:var(--font-mono);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:1.35;--tw-font-weight:var(--font-weight-bold);line-height:1.35;font-weight:var(--font-weight-bold);color:var(--text-primary)}.bonus-row{align-items:center;gap:calc(var(--spacing) * 2.5);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);min-height:58px;padding-inline:calc(var(--spacing) * 3);background-color:#ffffff05;grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr);padding-block:11px;display:grid}.empty-state{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.45;color:var(--text-secondary);background-color:#ffffff05;padding:13px;line-height:1.45}.plan-card{width:100%;min-height:44px;min-width:calc(var(--spacing) * 0);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:center;--tw-leading:var(--leading-tight);font-size:14px;line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);color:var(--text-primary);transition-property:transform,border-color,background,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#ffffff05;justify-content:center;align-items:center;display:inline-flex}@media (hover:hover){.plan-card:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.plan-card:hover{border-color:color-mix(in srgb,var(--accent) 42%,var(--border))}}.plan-card:hover{background-color:var(--bg-card-hover)}}.plan-card{transition-duration:.2s}.plan-card-active{border-color:var(--accent);background-color:color-mix(in srgb,var(--accent) 8%,transparent);color:var(--accent);--tw-shadow:0 12px 30px var(--tw-shadow-color,#0003);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:color-mix(in srgb,var(--accent) 58%,transparent)}.payment-method-card{width:100%;min-height:52px;min-width:calc(var(--spacing) * 0);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 4);text-align:center;--tw-leading:var(--leading-tight);font-size:15px;line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);color:var(--text-primary);transition-property:transform,border-color,background,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#ffffff08;justify-content:center;align-items:center;padding-block:12px;display:inline-flex}@media (hover:hover){.payment-method-card:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.payment-method-card:hover{border-color:color-mix(in srgb,var(--accent) 42%,var(--border))}}.payment-method-card:hover{background-color:var(--bg-card-hover)}}.payment-method-card{transition-duration:.2s}.payment-method-card--platega{background:#00fe7a14;border-color:#00fe7a5c}.payment-method-card--cryptopay{background:#22d3ee14;border-color:#22d3ee5c}.plan-name{--tw-leading:var(--leading-tight);font-size:15px;line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);display:block}.plan-meta{margin-top:calc(var(--spacing) * 1);font-family:var(--font-mono);--tw-leading:1.3;--tw-font-weight:var(--font-weight-bold);font-size:11px;line-height:1.3;font-weight:var(--font-weight-bold);color:var(--text-muted);display:block}.plan-price{text-align:right;max-width:48%;font-family:var(--font-mono);--tw-leading:var(--leading-tight);font-size:15px;line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);color:var(--accent);flex:none;display:block}.notice{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.45;color:var(--text-secondary);background-color:#ffffff05;padding:13px;line-height:1.45}.step-num{font-family:var(--font-mono);--tw-leading:1.1;--tw-font-weight:var(--font-weight-extrabold);font-size:11px;line-height:1.1;font-weight:var(--font-weight-extrabold);color:currentColor;display:block}.step-name{margin-top:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:1.15;--tw-font-weight:var(--font-weight-extrabold);line-height:1.15;font-weight:var(--font-weight-extrabold);color:currentColor;display:block}.main-value{margin:calc(var(--spacing) * 0);--tw-leading:1.08;--tw-font-weight:var(--font-weight-extrabold);font-size:32px;line-height:1.08;font-weight:var(--font-weight-extrabold);color:var(--text-primary)}.main-caption{margin-top:calc(var(--spacing) * 1.5);margin-bottom:calc(var(--spacing) * 0);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.45;color:var(--text-secondary);line-height:1.45}.status-text{margin:calc(var(--spacing) * 0);min-height:calc(var(--spacing) * 5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.45;color:var(--text-secondary);line-height:1.45}.legal-links{justify-content:center;column-gap:calc(var(--spacing) * 4);row-gap:calc(var(--spacing) * 1.5);text-align:center;flex-wrap:wrap;display:flex}.app-header{min-height:calc(var(--spacing) * 12);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.app-header-title{min-width:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 2.5);display:flex}.app-header-actions{min-width:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 2);display:flex}.panel-head-sm{min-width:calc(var(--spacing) * 0);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 2.5);display:flex}.user-menu,.lang-menu{display:inline-flex;position:relative}.lang-chip{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);min-height:38px;padding-inline:calc(var(--spacing) * 2);text-align:left;--tw-font-weight:var(--font-weight-extrabold);font-size:12px;font-weight:var(--font-weight-extrabold);color:var(--text-primary);transition-property:transform,border-color,background,color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.4,0,.2,1);background-color:#ffffff08;text-decoration-line:none;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-flex}@media (hover:hover){.lang-chip:hover{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.lang-chip:hover{border-color:color-mix(in srgb,var(--accent) 42%,var(--border))}}.lang-chip:hover{background-color:var(--bg-card-hover)}}.lang-chip[aria-expanded=true]{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.lang-chip[aria-expanded=true]{border-color:color-mix(in srgb,var(--accent) 55%,var(--border))}}.lang-chip[aria-expanded=true]{background-color:var(--bg-card-hover)}.lang-chip-flag{--tw-leading:1;font-family:Twemoji Country Flags,Apple Color Emoji,Segoe UI Emoji,Noto Color Emoji,sans-serif;font-size:14px;line-height:1}.lang-chip-label{font-family:var(--font-mono);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);font-size:11px}.lang-chip-caret{height:calc(var(--spacing) * 3);width:calc(var(--spacing) * 3);fill:var(--text-secondary);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex:none}.lang-chip[aria-expanded=true] .lang-chip-caret{transform:rotate(180deg)}.lang-dropdown{pointer-events:none;top:calc(100% + 8px);right:calc(var(--spacing) * 0);z-index:30;transform-origin:100% 0;--tw-translate-y:calc(var(--spacing) * -2);width:160px;translate:var(--tw-translate-x) var(--tw-translate-y);gap:calc(var(--spacing) * 0);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg-card);opacity:0;--tw-shadow:0 24px 48px var(--tw-shadow-color,#00000073);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.2,.8,.2,1);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);will-change:opacity, transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.2,.8,.2,1);display:grid;position:absolute;scale:.96}.lang-dropdown.show{pointer-events:auto;--tw-translate-y:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y);--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:1}.lang-dropdown-item{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2.5);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);text-align:left;--tw-font-weight:var(--font-weight-bold);font-size:13px;font-weight:var(--font-weight-bold);color:var(--text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#0000;border-width:0;display:flex}@media (hover:hover){.lang-dropdown-item:hover{background-color:var(--bg-card-hover);color:var(--text-primary)}}.lang-dropdown-item+.lang-dropdown-item{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border)}.lang-dropdown-item.is-active{background-color:var(--bg-card-hover);color:var(--text-primary)}.lang-dropdown-flag{--tw-leading:1;font-family:Twemoji Country Flags,Apple Color Emoji,Segoe UI Emoji,Noto Color Emoji,sans-serif;font-size:16px;line-height:1}.lang-dropdown-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.user-chip{max-width:200px;min-height:38px;min-width:calc(var(--spacing) * 0);cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding-block:calc(var(--spacing) * 1);padding-right:calc(var(--spacing) * 2.5);padding-left:calc(var(--spacing) * 1);text-align:left;--tw-font-weight:var(--font-weight-extrabold);font-size:13px;font-weight:var(--font-weight-extrabold);color:var(--text-primary);transition-property:transform,border-color,background,color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.4,0,.2,1);background-color:#ffffff08;text-decoration-line:none;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-flex}@media (hover:hover){.user-chip:hover{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.user-chip:hover{border-color:color-mix(in srgb,var(--accent) 42%,var(--border))}}.user-chip:hover{background-color:var(--bg-card-hover)}}.user-chip[aria-expanded=true]{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.user-chip[aria-expanded=true]{border-color:color-mix(in srgb,var(--accent) 55%,var(--border))}}.user-chip[aria-expanded=true]{background-color:var(--bg-card-hover)}.user-chip-avatar{border-radius:var(--radius-sm);object-fit:cover;background:var(--accent);flex:none;width:28px;height:28px}@supports (color:color-mix(in lab, red, red)){.user-chip-avatar{background:color-mix(in srgb, var(--accent) 10%, var(--bg-card))}}.user-chip-avatar{image-rendering:pixelated}.user-chip-name{min-width:calc(var(--spacing) * 0);text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.user-chip-caret{height:calc(var(--spacing) * 3);width:calc(var(--spacing) * 3);fill:var(--text-secondary);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));flex:none}.user-chip[aria-expanded=true] .user-chip-caret{transform:rotate(180deg)}.user-dropdown{pointer-events:none;top:calc(100% + 8px);right:calc(var(--spacing) * 0);z-index:30;transform-origin:100% 0;--tw-translate-y:calc(var(--spacing) * -2);width:min(100vw - 28px,280px);translate:var(--tw-translate-x) var(--tw-translate-y);gap:calc(var(--spacing) * 0);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg-card);opacity:0;--tw-shadow:0 24px 48px var(--tw-shadow-color,#00000073);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.2,.8,.2,1);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);will-change:opacity, transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.2,.8,.2,1);display:grid;position:absolute;scale:.96}.user-dropdown.show{pointer-events:auto;--tw-translate-y:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y);--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:1}.user-dropdown-head{align-items:center;gap:calc(var(--spacing) * 2.5);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);display:flex}.user-dropdown-avatar{border-radius:var(--radius-md);object-fit:cover;background:var(--accent);flex:none;width:40px;height:40px}@supports (color:color-mix(in lab, red, red)){.user-dropdown-avatar{background:color-mix(in srgb, var(--accent) 10%, var(--bg-card))}}.user-dropdown-avatar{image-rendering:pixelated}.user-dropdown-head-text{min-width:calc(var(--spacing) * 0);gap:calc(var(--spacing) * .5);display:grid}.user-dropdown-name{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);text-overflow:ellipsis;white-space:nowrap;color:var(--text-primary);overflow:hidden}.user-dropdown-sub{font-family:var(--font-mono);text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:11px;overflow:hidden}.user-dropdown-rows{display:grid}.user-dropdown-row{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);display:flex}.user-dropdown-row+.user-dropdown-row{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border)}.user-dropdown-row-label{font-family:var(--font-mono);--tw-font-weight:var(--font-weight-bold);font-size:11px;font-weight:var(--font-weight-bold);color:var(--text-muted)}.user-dropdown-row-status{text-align:right;--tw-font-weight:var(--font-weight-bold);font-size:13px;font-weight:var(--font-weight-bold);text-overflow:ellipsis;white-space:nowrap;color:var(--text-primary);overflow:hidden}.user-dropdown-row-status.linked{color:#6ee7b7}.user-dropdown-row-status.unlinked{color:var(--text-muted)}.user-dropdown-logout{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:0;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);width:100%;min-height:42px;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);text-align:center;--tw-font-weight:var(--font-weight-extrabold);font-size:13px;font-weight:var(--font-weight-extrabold);color:var(--text-secondary);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#0000;border-radius:0;display:flex}@media (hover:hover){.user-dropdown-logout:hover{background-color:var(--bg-card-hover);color:var(--text-primary)}}.user-dropdown-logout svg{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);fill:currentColor}.sub-status{align-items:center;gap:calc(var(--spacing) * 2.5);min-height:28px;color:var(--danger);display:flex}.sub-status.active{color:var(--success)}.sub-status-dot{background:currentColor;border-radius:3.40282e38px;flex:none;width:10px;height:10px;display:inline-block;position:relative;box-shadow:0 0 12px}.sub-status-dot:after{content:"";opacity:.55;background:currentColor;border-radius:9999px;animation:1.8s cubic-bezier(.4,0,.6,1) infinite sub-status-pulse;position:absolute;inset:0}.sub-status-text{--tw-leading:var(--leading-tight);font-size:15px;line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);letter-spacing:.01em}@keyframes brand-logo-spin{to{transform:rotate(360deg)}}.sub-countdown{gap:calc(var(--spacing) * 1);display:grid}.sub-countdown-row{align-items:baseline;column-gap:calc(var(--spacing) * 2);row-gap:calc(var(--spacing) * .5);flex-wrap:wrap;display:flex}.sub-countdown-value{--tw-leading:1.1;--tw-font-weight:var(--font-weight-extrabold);font-size:22px;line-height:1.1;font-weight:var(--font-weight-extrabold);color:var(--text-primary)}.sub-countdown-caption{font-family:var(--font-mono);--tw-leading:1.3;color:var(--text-muted);font-size:12px;line-height:1.3}.traffic-bar{border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background:#ffffff05;border-radius:3.40282e38px;justify-content:center;align-items:center;width:100%;min-height:24px;display:flex;position:relative;overflow:hidden}.traffic-bar-fill{inset-block:calc(var(--spacing) * 0);left:calc(var(--spacing) * 0);background:var(--accent);width:0%;position:absolute}@supports (color:color-mix(in lab, red, red)){.traffic-bar-fill{background:color-mix(in srgb, var(--accent) 28%, transparent)}}.traffic-bar-fill{transition:width .3s cubic-bezier(.4,0,.2,1),background .2s}.traffic-bar.over .traffic-bar-fill{background:var(--danger)}@supports (color:color-mix(in lab, red, red)){.traffic-bar.over .traffic-bar-fill{background:color-mix(in srgb, var(--danger) 32%, transparent)}}.traffic-bar.unlimited .traffic-bar-fill{background:var(--success);width:100%}@supports (color:color-mix(in lab, red, red)){.traffic-bar.unlimited .traffic-bar-fill{background:color-mix(in srgb, var(--success) 12%, transparent)}}.traffic-bar.inactive{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.traffic-bar.inactive{border-color:color-mix(in srgb, var(--danger) 60%, transparent)}}.traffic-bar.inactive .traffic-bar-fill{background:var(--danger);width:100%}@supports (color:color-mix(in lab, red, red)){.traffic-bar.inactive .traffic-bar-fill{background:color-mix(in srgb, var(--danger) 55%, transparent)}}.traffic-bar-value{z-index:1;min-width:calc(var(--spacing) * 0);padding-inline:calc(var(--spacing) * 3);text-align:center;font-family:var(--font-mono);--tw-font-weight:var(--font-weight-bold);font-size:12px;font-weight:var(--font-weight-bold);white-space:nowrap;color:#fff;font-variant-numeric:tabular-nums;position:relative}.traffic-bar.unlimited .traffic-bar-value{animation:1.8s ease-in-out infinite traffic-infinity-pulse}@keyframes traffic-infinity-pulse{0%,to{opacity:1;text-shadow:0 0 0 color-mix(in srgb, var(--success) 0%, transparent)}50%{opacity:.7;text-shadow:0 0 10px color-mix(in srgb, var(--success) 70%, transparent)}}.link-cta-head{gap:calc(var(--spacing) * 1);display:grid}.link-cta-title{--tw-leading:var(--leading-tight);font-size:15px;line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);color:var(--text-primary)}.link-cta-caption{--tw-leading:1.4;color:var(--text-secondary);font-size:13px;line-height:1.4}@keyframes sub-status-pulse{0%{opacity:.55;transform:scale(1)}70%{opacity:0;transform:scale(2.2)}to{opacity:0;transform:scale(2.2)}}.login-head{min-width:calc(var(--spacing) * 0);justify-content:center;align-items:center;gap:calc(var(--spacing) * 3.5);display:flex}.btn{min-height:46px;min-width:calc(var(--spacing) * 0);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);text-align:center;--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);color:var(--text-primary);transition-property:transform,border-color,background,color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.4,0,.2,1);background-color:#ffffff08;padding-block:11px;padding-inline:13px;text-decoration-line:none;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-flex}@media (hover:hover){.btn:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.btn:hover{border-color:color-mix(in srgb,var(--accent) 42%,var(--border))}}.btn:hover{background-color:var(--bg-card-hover)}}.btn{overflow-wrap:anywhere}.btn-primary{min-height:46px;min-width:calc(var(--spacing) * 0);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--accent);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn-primary{border-color:color-mix(in srgb,var(--accent) 80%,var(--border))}}.btn-primary{background-color:var(--accent);text-align:center;--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);color:#04110a;--tw-shadow:0 10px 26px var(--tw-shadow-color,var(--accent));padding-block:11px;padding-inline:13px;text-decoration-line:none}@supports (color:color-mix(in lab, red, red)){.btn-primary{--tw-shadow:0 10px 26px var(--tw-shadow-color,color-mix(in srgb,var(--accent) 18%,transparent))}}.btn-primary{box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition-property:transform,border-color,background,color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.4,0,.2,1);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}@media (hover:hover){.btn-primary:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);background-color:var(--accent-hover);--tw-shadow:0 14px 34px var(--tw-shadow-color,var(--accent))}@supports (color:color-mix(in lab, red, red)){.btn-primary:hover{--tw-shadow:0 14px 34px var(--tw-shadow-color,color-mix(in srgb,var(--accent) 24%,transparent))}}.btn-primary:hover{box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.btn-primary{overflow-wrap:anywhere}.btn-ghost{width:100%;min-height:46px;min-width:calc(var(--spacing) * 0);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--accent);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn-ghost{border-color:color-mix(in srgb,var(--accent) 35%,var(--border))}}.btn-ghost{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.btn-ghost{background-color:color-mix(in srgb,var(--accent) 5%,transparent)}}.btn-ghost{text-align:center;--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);color:var(--accent);--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition-property:transform,border-color,background,color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.4,0,.2,1);padding-block:11px;padding-inline:13px;text-decoration-line:none;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}@media (hover:hover){.btn-ghost:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.btn-ghost:hover{background-color:color-mix(in srgb,var(--accent) 10%,transparent)}}.btn-ghost:hover{--tw-shadow:0 10px 24px var(--tw-shadow-color,var(--accent))}@supports (color:color-mix(in lab, red, red)){.btn-ghost:hover{--tw-shadow:0 10px 24px var(--tw-shadow-color,color-mix(in srgb,var(--accent) 10%,transparent))}}.btn-ghost:hover{box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.btn-ghost{overflow-wrap:anywhere}.icon-btn{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);width:38px;height:38px;color:var(--text-secondary);transition-property:transform,border-color,background,color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.4,0,.2,1);background-color:#ffffff08;flex:none;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-flex}@media (hover:hover){.icon-btn:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.icon-btn:hover{border-color:color-mix(in srgb,var(--accent) 42%,var(--border))}}.icon-btn:hover{background-color:var(--bg-card-hover)}}.input{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);width:100%;min-height:46px;padding-inline:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--text-primary);transition-property:border-color,background,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;--tw-ease:cubic-bezier(.4,0,.2,1);--tw-outline-style:none;background-color:#05080d6b;outline-style:none;padding-block:11px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.input::placeholder{color:var(--text-muted)}.input:focus{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.input:focus{border-color:color-mix(in srgb,var(--accent) 55%,var(--border))}}.input:focus{--tw-shadow:0 0 0 3px var(--tw-shadow-color,var(--accent)), 0 12px 28px var(--tw-shadow-color,#0003);background-color:#070c13ad}@supports (color:color-mix(in lab, red, red)){.input:focus{--tw-shadow:0 0 0 3px var(--tw-shadow-color,color-mix(in srgb,var(--accent) 12%,transparent)), 0 12px 28px var(--tw-shadow-color,#0003)}}.input:focus{box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.input[aria-invalid=true]{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.input[aria-invalid=true]{border-color:color-mix(in srgb,var(--danger) 72%,var(--border))}}.input[aria-invalid=true]:focus{--tw-shadow:0 0 0 3px var(--tw-shadow-color,var(--danger))}@supports (color:color-mix(in lab, red, red)){.input[aria-invalid=true]:focus{--tw-shadow:0 0 0 3px var(--tw-shadow-color,color-mix(in srgb,var(--danger) 14%,transparent))}}.input[aria-invalid=true]:focus{box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.input-code{text-align:center;font-family:var(--font-mono);--tw-tracking:0;letter-spacing:0}.panel{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg-card);--tw-shadow:0 20px 40px var(--tw-shadow-color,#0000002e);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);position:relative;overflow:hidden}.panel-modal{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg-card);--tw-shadow:0 30px 60px var(--tw-shadow-color,#00000047);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);position:relative;overflow:hidden}.step-box{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);min-height:58px;color:var(--text-muted);--tw-shadow:inset 0 1px 0 var(--tw-shadow-color,#ffffff0b);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);background-color:#ffffff05;padding:9px}.modal{pointer-events:none;inset:calc(var(--spacing) * 0);z-index:20;opacity:0;transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));padding:calc(max(var(--app-safe-top), 14px) + var(--app-safe-top-extra)) 14px max(var(--app-safe-bottom), 14px);place-items:center;display:grid;position:fixed}.modal.show{pointer-events:auto;opacity:1}.modal-backdrop,.tools-modal-backdrop{inset:calc(var(--spacing) * 0);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);background:#03060ab8;border-width:0;position:absolute}.tools-modal-backdrop{z-index:25;position:fixed}.modal-card{width:min(100%,560px);max-height:min(86vh,760px);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.22s;--tw-ease:cubic-bezier(.2,.8,.2,1);transition-duration:.22s;transition-timing-function:cubic-bezier(.2,.8,.2,1);position:relative;overflow:auto;transform:translateY(18px)scale(.98)}.modal.show .modal-card{transform:translateY(0)scale(1)}.app{gap:calc(var(--spacing) * 4);width:min(100vw,560px);max-width:100vw;padding:calc(max(var(--app-safe-top), 14px) + var(--app-safe-top-extra)) 14px max(var(--app-safe-bottom), 22px);flex-direction:column;margin-inline:auto;display:flex}@media (min-width:561px){.app{justify-content:center;min-height:100vh}}.telegram-login-widget{min-width:calc(var(--spacing) * 0);align-items:center;display:flex}.brand-logo-shell{flex:none;place-items:center;width:38px;height:38px;display:grid;position:relative;overflow:hidden}.brand-logo-shell--lg{width:96px;height:96px}.brand-logo{inset:calc(var(--spacing) * 0);object-fit:contain;width:100%;height:100%;position:absolute}.brand-logo-spinner{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);border-style:var(--tw-border-style);border-width:2px;border-color:var(--accent);border-radius:3.40282e38px;flex:none}@supports (color:color-mix(in lab, red, red)){.brand-logo-spinner{border-color:color-mix(in srgb, var(--accent) 18%, var(--border))}}.brand-logo-spinner{border-top-color:var(--accent);animation:.85s linear infinite brand-logo-spin}.brand-logo--lg{width:84px;height:84px}.plans,.methods,.metrics,.referral-link-list,.bonus-list{min-width:calc(var(--spacing) * 0);display:grid}.actions,.otp-slots,.metric{min-width:calc(var(--spacing) * 0);gap:calc(var(--spacing) * 2);display:grid}.actions{grid-template-columns:minmax(0,1fr) 48px}.metric{grid-template-columns:minmax(0,.88fr) minmax(0,1.12fr)}.metrics{gap:calc(var(--spacing) * 0);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background:#ffffff05;overflow:hidden}.metric,.notice,.otp-slots span{border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background:#ffffff05}.metric{align-items:center;gap:calc(var(--spacing) * 2.5);min-height:46px;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);min-height:calc(var(--spacing) * 12)}.metric+.metric{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border)}.brand-title,.login-brand-title{max-width:260px;font-family:var(--font-mono);--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold);--tw-tracking:0;letter-spacing:0;color:var(--accent)}.brand-title{--tw-leading:1.15;text-overflow:ellipsis;white-space:nowrap;text-shadow:0 0 20px var(--accent);font-size:17px;line-height:1.15;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.brand-title{text-shadow:0 0 20px color-mix(in srgb, var(--accent) 18%, transparent)}}.login-brand-title{text-align:left;font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-leading:1.1;overflow-wrap:anywhere;line-height:1.1}.login-auth-body{align-content:flex-start;gap:calc(var(--spacing) * 2.5);width:100%;min-height:140px;display:grid}.login-auth-body--telegram{align-content:center}.login-text,.promo-status{color:var(--text-secondary)}.section-label{min-width:calc(var(--spacing) * 0);font-family:var(--font-mono);--tw-font-weight:var(--font-weight-extrabold);font-size:11px;font-weight:var(--font-weight-bold);color:var(--text-muted);overflow-wrap:anywhere;font-weight:var(--font-weight-extrabold)}.mono-pill,.badge{min-height:calc(var(--spacing) * 7);border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 2.5);font-family:var(--font-mono);--tw-font-weight:var(--font-weight-bold);font-size:11px;font-weight:var(--font-weight-bold);white-space:nowrap;align-items:center;display:inline-flex;box-shadow:inset 0 1px #ffffff0f}.mono-pill{color:var(--accent);border:1px solid var(--accent)}@supports (color:color-mix(in lab, red, red)){.mono-pill{border:1px solid color-mix(in srgb, var(--accent) 45%, var(--border))}}.mono-pill{background:var(--accent)}@supports (color:color-mix(in lab, red, red)){.mono-pill{background:color-mix(in srgb, var(--accent) 9%, transparent)}}.badge{min-width:calc(var(--spacing) * 0);text-overflow:ellipsis;color:#6ee7b7;border:1px solid var(--success);overflow:hidden}@supports (color:color-mix(in lab, red, red)){.badge{border:1px solid color-mix(in srgb, var(--success) 34%, var(--border))}}.badge{background:#10b9811f}.badge.off,.promo-status.error,.login-status.error{color:#fca5a5}.badge.off{border-color:var(--danger)}@supports (color:color-mix(in lab, red, red)){.badge.off{border-color:color-mix(in srgb, var(--danger) 34%, var(--border))}}.badge.off{background:#ef44441f}.promo-status.success{color:#6ee7b7}#legal-links-login{width:min(100vw,560px)}.legal-link,.code-modal-resend{border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);--tw-font-weight:var(--font-weight-bold);font-size:13px;font-weight:var(--font-weight-bold);color:var(--text-secondary);text-underline-offset:3px;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));text-decoration-line:underline;-webkit-text-decoration-color:var(--accent);-webkit-text-decoration-color:var(--accent);text-decoration-color:var(--accent);background-color:#0000;border-width:0}@supports (color:color-mix(in lab, red, red)){.legal-link,.code-modal-resend{-webkit-text-decoration-color:color-mix(in srgb, var(--accent) 42%, currentColor);-webkit-text-decoration-color:color-mix(in srgb, var(--accent) 42%, currentColor);text-decoration-color:color-mix(in srgb, var(--accent) 42%, currentColor)}}.legal-link{flex:0 auto;display:inline}.otp-input{inset:calc(var(--spacing) * 0);z-index:2;border-style:var(--tw-border-style);color:#0000;opacity:.01;--tw-outline-style:none;caret-color:#0000;background-color:#0000;border-width:0;outline-style:none;width:100%;height:100%;position:absolute}.otp-slots{grid-template-columns:repeat(6,minmax(0,1fr))}.otp-slots span{aspect-ratio:1;min-width:calc(var(--spacing) * 0);border-radius:var(--radius-md);font-family:var(--font-mono);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-leading:1;--tw-font-weight:850;color:var(--text-primary);transition-property:border-color,background,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background:#05080d6b;place-items:center;font-weight:850;line-height:1;display:grid}.otp-slots span.filled{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.otp-slots span.filled{border-color:color-mix(in srgb, var(--accent) 35%, var(--border))}}.otp-slots span.filled{background:var(--accent)}@supports (color:color-mix(in lab, red, red)){.otp-slots span.filled{background:color-mix(in srgb, var(--accent) 7%, #ffffff09)}}.otp-slots span.active{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.otp-slots span.active{border-color:color-mix(in srgb, var(--accent) 72%, var(--border))}}.otp-slots span.active{box-shadow:0 0 0 3px var(--accent)}@supports (color:color-mix(in lab, red, red)){.otp-slots span.active{box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent)}}.step-num,.step-name{color:currentColor;display:block}.step-num{font-family:var(--font-mono);--tw-leading:1.1;--tw-font-weight:var(--font-weight-extrabold);font-size:11px;line-height:1.1;font-weight:var(--font-weight-extrabold)}.step-name{margin-top:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:1.15;--tw-font-weight:var(--font-weight-extrabold);line-height:1.15;font-weight:var(--font-weight-extrabold)}.step.active,.auth-tab.active{border-color:var(--accent);color:var(--accent);background:var(--accent)}@supports (color:color-mix(in lab, red, red)){.step.active,.auth-tab.active{background:color-mix(in srgb, var(--accent) 9%, transparent)}}.step.done{color:var(--text-secondary);border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.step.done{border-color:color-mix(in srgb, var(--accent) 45%, var(--border))}}.notice{border-radius:var(--radius-md);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.45;color:var(--text-secondary);padding:13px;line-height:1.45}.login-layout.show{display:flex}.telegram-login-widget{min-height:calc(var(--spacing) * 14);justify-content:center}.login-text,.promo-status{margin:calc(var(--spacing) * 0);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:1.45;line-height:1.45}.promo-status{margin-top:calc(var(--spacing) * .5);overflow-wrap:anywhere;min-height:22px}.login-status{margin:calc(var(--spacing) * 0);min-height:calc(var(--spacing) * 5)}.toast{z-index:40;border-radius:var(--radius-md);--tw-font-weight:var(--font-weight-bold);width:max-content;max-width:calc(100vw - 28px);font-size:13px;font-weight:var(--font-weight-bold);color:var(--text-primary);opacity:0;transition-property:opacity,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.3s;--tw-ease:cubic-bezier(.2,.8,.2,1);left:50%;bottom:max(var(--app-safe-bottom), 22px);border:1px solid var(--accent);padding-block:11px;padding-inline:13px;transition-duration:.3s;transition-timing-function:cubic-bezier(.2,.8,.2,1);position:fixed}@supports (color:color-mix(in lab, red, red)){.toast{border:1px solid color-mix(in srgb, var(--accent) 45%, var(--border))}}.toast{-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);box-shadow:0 16px 38px #00000073, 0 0 15px var(--accent);background:#0f1521d1}@supports (color:color-mix(in lab, red, red)){.toast{box-shadow:0 16px 38px #00000073, 0 0 15px color-mix(in srgb, var(--accent) 15%, transparent)}}.toast{pointer-events:none;transform:translate(-50%,14px)scale(.94)}.toast.show{opacity:1;transform:translate(-50%)scale(1)}}@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.z-\[1\]{z-index:1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-5{height:calc(var(--spacing) * 5)}.min-h-14{min-height:calc(var(--spacing) * 14)}.w-5{width:calc(var(--spacing) * 5)}.w-12{width:calc(var(--spacing) * 12)}.w-\[min\(100\%\,420px\)\]{width:min(100%,420px)}.w-full{width:100%}.max-w-\[320px\]{max-width:320px}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-12{min-width:calc(var(--spacing) * 12)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-text{cursor:text}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[minmax\(0\,0\.6fr\)_minmax\(0\,1\.4fr\)\]{grid-template-columns:minmax(0,.6fr) minmax(0,1.4fr)}.grid-cols-\[minmax\(0\,0\.8fr\)_minmax\(0\,1\.2fr\)\]{grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr)}.grid-cols-\[minmax\(0\,1fr\)_minmax\(120px\,auto\)\]{grid-template-columns:minmax(0,1fr) minmax(120px,auto)}.justify-items-center{justify-items:center}.justify-items-stretch{justify-items:stretch}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-\[17px\]{gap:17px}.rounded{border-radius:.25rem}.rounded-\[var\(--radius-md\)\]{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-\[var\(--border\)\]{border-color:var(--border)}.bg-\[rgba\(255\,255\,255\,0\.03\)\]{background-color:#ffffff08}.bg-\[rgba\(255\,255\,255\,0\.015\)\]{background-color:#ffffff04}.fill-current{fill:currentColor}.p-0{padding:calc(var(--spacing) * 0)}.p-3\.5{padding:calc(var(--spacing) * 3.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-\[17px\]{padding:17px}.p-\[18px\]{padding:18px}.px-0{padding-inline:calc(var(--spacing) * 0)}.pt-3\.5{padding-top:calc(var(--spacing) * 3.5)}.text-center{text-align:center}.font-\[family-name\:var\(--font-mono\)\]{font-family:var(--font-mono)}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[20px\]{font-size:20px}.text-\[22px\]{font-size:22px}.text-\[28px\]{font-size:28px}.leading-\[1\.1\]{--tw-leading:1.1;line-height:1.1}.leading-\[1\.05\]{--tw-leading:1.05;line-height:1.05}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-\[1\.45\]{--tw-leading:1.45;line-height:1.45}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.tracking-\[0\]{--tw-tracking:0;letter-spacing:0}.break-words{overflow-wrap:break-word}.text-\[var\(--accent\)\]{color:var(--accent)}.text-\[var\(--text-primary\)\]{color:var(--text-primary)}.text-\[var\(--text-secondary\)\]{color:var(--text-secondary)}.uppercase{text-transform:uppercase}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}@media not all and (min-width:560px){.max-\[560px\]\:flex-\[1_1_100\%\]{flex:100%}}@media not all and (min-width:460px){.max-\[460px\]\:max-w-\[240px\]{max-width:240px}.max-\[460px\]\:text-\[24px\]{font-size:24px}}@media (min-width:40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}}:root{color-scheme:dark;--bg-primary:#05070a;--bg-card:#0f1521;--bg-card-hover:#161e2e;--border:#1e2940;--text-primary:#e2e8f0;--text-secondary:#94a3b8;--text-muted:#64748b;--accent:#00fe7a;--accent-hover:#5aff9f;--success:#10b981;--danger:#ef4444;--font-sans:Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono:"JetBrains Mono", "Fira Code", monospace;--radius-sm:6px;--radius-md:8px;--radius-lg:8px;--transition:.2s cubic-bezier(.4, 0, .2, 1);--app-safe-top-extra:10px;--app-safe-top:max(var(--tg-content-safe-area-inset-top,0px), var(--tg-safe-area-inset-top,0px), env(safe-area-inset-top));--app-safe-bottom:max(var(--tg-content-safe-area-inset-bottom,0px), var(--tg-safe-area-inset-bottom,0px), env(safe-area-inset-bottom))}.hidden{display:none!important}@supports not (color:color-mix(in srgb, #000 50%, #fff)){.mono-pill,.step.active,.auth-tab.active{border-color:var(--accent);background:#00fe7a14}}@media (hover:hover) and (pointer:fine){.legal-link:hover:not(:disabled),.code-modal-resend:hover:not(:disabled){color:var(--accent)}}@media (max-width:460px){.app{padding-inline:calc(var(--spacing) * 2.5)}.user-chip{max-width:150px}.user-chip-name{display:none}.brand-title{max-width:164px}.metric{grid-template-columns:repeat(1,minmax(0,1fr))}.badge,.mono-pill{max-width:100%}.otp-slots{gap:calc(var(--spacing) * 1.5)}.otp-slots span{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.brand-logo--lg{width:68px;height:68px}.brand-logo-shell--lg{width:96px;height:96px}.login-auth-body{min-height:128px}.login-brand-title{max-width:200px;font-size:21px}}@media (max-width:380px){.brand-title{max-width:150px}}@media (max-width:560px){.step-box{min-height:54px;padding-inline:7px;padding-block:calc(var(--spacing) * 2)}.modal{padding:calc(var(--spacing) * 0);padding:calc(max(var(--app-safe-top), 14px) + var(--app-safe-top-extra)) 14px max(var(--app-safe-bottom), 14px);place-items:center;top:0;bottom:0}.modal--page{padding:calc(var(--spacing) * 0);place-items:stretch stretch;padding:0!important}.modal--page .modal-backdrop{background-color:var(--bg-primary);opacity:1;-webkit-backdrop-filter:none;backdrop-filter:none}.modal-card{border-radius:var(--radius-lg);width:100%;height:auto;max-height:min(92vh,760px);transform:translateY(12px)scale(.98)}.modal--page .modal-card{border-style:var(--tw-border-style);--tw-shadow:0 0 #0000;width:100%;max-width:none;height:100%;max-height:none;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);border-width:0;border-radius:0;transform:none!important}.modal.show .modal-card{transform:translateY(0)scale(1)}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1} \ No newline at end of file +:root { + color-scheme: dark; + --accent: #00fe7a; + --bg: #03070b; + --panel: #111820; + --panel-2: #0b1118; + --panel-3: #17212b; + --border: rgba(255, 255, 255, 0.12); + --border-strong: rgba(255, 255, 255, 0.2); + --text: #f2f7f4; + --muted: #a9b4b0; + --dim: #68736f; + --danger: #ff6b6b; + --blue: #2d9cff; + --radius: 8px; + font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + min-height: 100%; + margin: 0; +} + +body { + background: #02070b; + color: var(--text); + -webkit-font-smoothing: antialiased; + letter-spacing: 0; +} + +button, +input { + font: inherit; + letter-spacing: 0; +} + +button { + cursor: pointer; +} + +button:disabled { + cursor: progress; + opacity: 0.58; +} + +a { + color: inherit; +} + +.app-shell { + min-height: 100dvh; + background: linear-gradient(180deg, #06100c 0%, #03070b 26%, #02060a 100%); +} + +.phone-screen { + position: relative; + width: min(100vw, 440px); + min-height: 100dvh; + margin: 0 auto; + overflow-x: hidden; + padding: max(16px, env(safe-area-inset-top)) 14px max(18px, env(safe-area-inset-bottom)); +} + +.content { + display: grid; + gap: 10px; +} + +.content.with-nav { + padding-bottom: 86px; +} + +.loader { + display: grid; + min-height: 100dvh; + place-items: center; + align-content: center; + gap: 14px; + color: var(--muted); + font-weight: 800; +} + +.app-header, +.preview-header, +.screen-head { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 12px; + min-height: 48px; +} + +.screen-head { + grid-template-columns: 42px minmax(0, 1fr) 42px; + margin-bottom: 6px; +} + +.center-title { + align-items: start; +} + +.center-copy { + min-width: 0; + text-align: center; +} + +.screen-head h1, +.center-copy h1 { + margin: 0; + color: var(--text); + font-size: 18px; + line-height: 1.16; + font-weight: 850; +} + +.screen-head p, +.center-copy p { + margin: 3px 0 0; + color: var(--muted); + font-size: 12px; + line-height: 1.3; +} + +.screen-head strong { + color: var(--accent); +} + +.brand-row { + display: flex; + min-width: 0; + align-items: center; + gap: 10px; +} + +.brand-row strong { + min-width: 0; + overflow: hidden; + color: var(--text); + font-size: 15px; + font-weight: 850; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accent-title .brand-row strong { + color: var(--accent); +} + +.brand-mark { + display: grid; + width: 38px; + height: 38px; + flex: 0 0 auto; + place-items: center; + color: var(--accent); + font-size: 26px; + line-height: 1; +} + +.brand-mark img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.brand-mark-lg { + width: 66px; + height: 66px; + font-size: 46px; +} + +.brand-mark-xl { + width: 96px; + height: 96px; + font-size: 70px; +} + +.card { + position: relative; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius); + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018)), + var(--panel); + box-shadow: + 0 18px 48px rgba(0, 0, 0, 0.22), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + padding: 14px; +} + +.card-active { + border-color: color-mix(in srgb, var(--accent) 76%, var(--border)); + box-shadow: + 0 18px 48px rgba(0, 0, 0, 0.22), + 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.card-compact { + padding: 11px; +} + +.status-card { + padding: 16px; +} + +.sub-status { + display: flex; + align-items: center; + gap: 12px; + color: var(--accent); +} + +.sub-status h2 { + margin: 0; + font-size: 15px; + line-height: 1.16; +} + +.sub-status p { + margin: 5px 0 0; + color: var(--text); + font-size: 12px; + opacity: 0.82; +} + +.traffic-top, +.total-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.traffic-top span, +.card-label, +.total-card span, +.settings-row small, +.select-card small, +.method-card small { + color: var(--muted); + font-size: 12px; +} + +.traffic-top strong { + font-size: 12px; + font-weight: 850; +} + +.progress { + position: relative; + height: 8px; + margin-top: 12px; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); +} + +.progress span { + position: absolute; + inset: 0 auto 0 0; + width: 0; + border-radius: inherit; + background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 72%, white)); + box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 40%, transparent); +} + +.traffic-percent { + margin-top: 6px; + text-align: right; + color: var(--muted); + font-size: 11px; +} + +.action-stack { + display: grid; + gap: 8px; +} + +.btn { + display: inline-flex; + min-height: 44px; + min-width: 0; + align-items: center; + justify-content: center; + gap: 8px; + border: 1px solid transparent; + border-radius: var(--radius); + padding: 10px 14px; + text-align: center; + text-decoration: none; + font-size: 14px; + font-weight: 850; + line-height: 1.1; + transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, color 0.16s ease; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn-primary { + border-color: color-mix(in srgb, var(--accent) 72%, transparent); + background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 94%, white), var(--accent)); + color: #031009; + box-shadow: 0 14px 34px color-mix(in srgb, var(--accent) 20%, transparent); +} + +.btn-secondary, +.btn-outline { + border-color: var(--border-strong); + background: rgba(255, 255, 255, 0.045); + color: var(--text); +} + +.btn-outline { + border-color: color-mix(in srgb, var(--accent) 60%, var(--border)); + color: var(--accent); +} + +.btn-ghost { + background: transparent; + color: var(--muted); +} + +.btn-telegram { + border-color: #2d9cff; + background: linear-gradient(180deg, #2f9ff4, #1786df); + color: white; +} + +.btn-icon { + border-color: var(--border); + background: rgba(255, 255, 255, 0.04); + color: var(--text); +} + +.btn-square { + width: 42px; + min-width: 42px; + height: 42px; + padding: 0; +} + +.btn-sm { + min-height: 36px; + padding: 8px 10px; + font-size: 12px; +} + +.wide { + width: 100%; +} + +.input { + width: 100%; + min-height: 44px; + min-width: 0; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(4, 8, 11, 0.48); + color: var(--text); + outline: none; + padding: 11px 12px; + font-size: 13px; + font-weight: 700; +} + +.input:focus { + border-color: color-mix(in srgb, var(--accent) 68%, var(--border)); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent); +} + +.input.muted { + display: flex; + align-items: center; + color: var(--dim); +} + +.bottom-action { + margin-top: 4px; +} + +.period-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; +} + +.period-card, +.method-card, +.select-card, +.settings-row { + border: 1px solid var(--border); + border-radius: var(--radius); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.018)); + color: var(--text); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045); +} + +.period-card { + position: relative; + display: grid; + min-height: 82px; + place-items: center; + gap: 4px; + padding: 10px 8px; +} + +.period-card strong { + font-size: 12px; + white-space: nowrap; +} + +.period-card span { + font-size: 16px; + font-weight: 850; +} + +.period-card small { + color: var(--muted); + font-size: 10px; +} + +.period-card svg { + position: absolute; + top: 8px; + right: 8px; + color: var(--accent); +} + +.period-card.active, +.method-card.active, +.select-card.active { + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 9%, rgba(255, 255, 255, 0.035)); + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--accent) 50%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.06); +} + +.total-card { + padding: 12px 14px; +} + +.total-card strong { + font-size: 22px; + font-weight: 900; +} + +.method-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 7px; +} + +.method-card { + display: flex; + min-height: 48px; + align-items: center; + gap: 8px; + padding: 8px; + text-align: left; +} + +.method-card svg { + flex: 0 0 auto; +} + +.method-card span { + display: grid; + min-width: 0; + gap: 2px; +} + +.method-card strong, +.method-card small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.method-card strong { + font-size: 10px; +} + +.method-card small { + font-size: 8px; +} + +.tariff-list, +.settings-list { + display: grid; + gap: 8px; +} + +.select-card { + display: grid; + grid-template-columns: 52px minmax(0, 1fr) 24px; + align-items: center; + gap: 12px; + padding: 12px; + text-align: left; +} + +.tariff-list.compact .select-card { + grid-template-columns: minmax(0, 1fr) auto 24px; +} + +.select-icon { + display: grid; + width: 42px; + height: 42px; + place-items: center; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--accent); +} + +.select-card span:not(.select-icon) { + display: grid; + gap: 3px; + min-width: 0; +} + +.select-card strong { + font-size: 14px; +} + +.select-card em { + color: var(--muted); + font-size: 11px; + font-style: normal; +} + +.copy-row, +.email-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; +} + +.copy-row code { + min-width: 0; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(0, 0, 0, 0.2); + color: var(--text); + padding: 12px; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bonus-card { + display: flex; + align-items: center; + gap: 14px; +} + +.bonus-card > svg { + flex: 0 0 auto; + color: var(--accent); +} + +.bonus-card span { + color: var(--muted); + font-size: 12px; +} + +.bonus-card strong { + display: block; + color: var(--accent); + font-size: 17px; +} + +.bonus-card p { + margin: 5px 0 0; + color: var(--muted); + font-size: 12px; +} + +.settings-profile { + display: flex; + align-items: center; + gap: 12px; +} + +.settings-avatar { + display: grid; + width: 58px; + height: 58px; + flex: 0 0 auto; + place-items: center; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 999px; + background: rgba(255, 255, 255, 0.03); +} + +.settings-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.settings-avatar svg { + color: var(--accent); +} + +.settings-profile-meta { + display: grid; + min-width: 0; + gap: 3px; +} + +.settings-profile-meta strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; +} + +.settings-profile-meta small { + overflow: hidden; + color: var(--muted); + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; +} + +.settings-row { + display: grid; + grid-template-columns: 28px minmax(0, 1fr) 18px; + align-items: center; + gap: 10px; + min-height: 50px; + padding: 9px 10px; + text-align: left; +} + +.settings-row > svg:first-child { + color: var(--text); + opacity: 0.9; +} + +.settings-row > svg:last-child { + color: var(--muted); +} + +.settings-row span { + display: grid; + min-width: 0; + gap: 2px; +} + +.settings-row strong, +.settings-row small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-row strong { + font-size: 12px; +} + +.bottom-nav { + position: absolute; + right: 14px; + bottom: max(10px, env(safe-area-inset-bottom)); + left: 14px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2px; + min-height: 64px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(7, 12, 17, 0.88); + box-shadow: 0 16px 38px rgba(0, 0, 0, 0.36); + backdrop-filter: blur(16px); +} + +.bottom-nav.static { + position: absolute; +} + +.bottom-nav button { + display: grid; + place-items: center; + align-content: center; + gap: 4px; + border: 0; + background: transparent; + color: var(--muted); + font-size: 10px; + font-weight: 800; +} + +.bottom-nav button.active { + color: var(--accent); +} + +.home-layout { + display: flex; + min-height: calc(100dvh - 34px); + flex-direction: column; + gap: 18px; + padding-bottom: 86px; +} + +.home-brand { + padding-top: clamp(26px, 10dvh, 74px); +} + +.home-bottom { + display: grid; + gap: 10px; + margin-top: auto; +} + +.auth-screen { + display: grid; + align-content: center; +} + +.auth-card-wrap { + display: grid; + gap: 16px; +} + +.login-brand { + display: grid; + justify-items: center; + gap: 10px; + text-align: center; +} + +.login-brand.small { + gap: 5px; +} + +.login-brand h1 { + margin: 0; + overflow-wrap: anywhere; + color: var(--accent); + font-size: 32px; + font-weight: 900; + line-height: 1.04; +} + +.login-brand p { + margin: 0; + color: var(--muted); + font-size: 13px; +} + +.auth-card { + display: grid; + gap: 14px; +} + +.segmented { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(0, 0, 0, 0.18); + padding: 4px; +} + +.segmented button { + min-height: 35px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--muted); + font-weight: 850; +} + +.segmented button.active { + background: color-mix(in srgb, var(--accent) 11%, transparent); + color: var(--accent); +} + +.auth-pane { + display: grid; + gap: 10px; +} + +.auth-pane .field-label, +.auth-card .field-label { + color: var(--text); + font-size: 13px; + font-weight: 850; +} + +.telegram-widget { + display: grid; + min-height: 54px; + place-items: center; +} + +.auth-bottom { + text-align: center; + color: var(--muted); + font-size: 13px; +} + +.auth-bottom strong { + color: var(--accent); +} + +.or-line { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 10px; + color: var(--muted); + font-size: 12px; +} + +.or-line span { + height: 1px; + background: var(--border); +} + +.otp-wrap { + display: grid; + align-content: center; + gap: 20px; + min-height: calc(100dvh - 120px); +} + +.otp-input-wrap { + position: relative; + display: block; +} + +.otp-input-wrap input { + position: absolute; + inset: 0; + z-index: 2; + width: 100%; + border: 0; + background: transparent; + color: transparent; + caret-color: transparent; + outline: none; +} + +.otp-slots { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 10px; +} + +.otp-slots span { + display: grid; + aspect-ratio: 1; + place-items: center; + border: 1px solid color-mix(in srgb, var(--accent) 74%, var(--border)); + border-radius: var(--radius); + background: rgba(255, 255, 255, 0.035); + font-size: 25px; + font-weight: 900; +} + +.otp-slots span.filled { + background: color-mix(in srgb, var(--accent) 9%, rgba(255, 255, 255, 0.04)); +} + +.link-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border: 0; + background: transparent; + color: var(--muted); + font-size: 13px; +} + +.status-line { + color: var(--accent); + font-size: 12px; + line-height: 1.4; +} + +.status-line.error, +.error { + color: var(--danger); +} + +.empty-card { + grid-column: 1 / -1; + color: var(--muted); + font-size: 13px; +} + +.dialog { + position: fixed; + inset: 0; + z-index: 40; + display: grid; + place-items: center; + padding: 16px; +} + +.dialog-backdrop { + position: absolute; + inset: 0; + border: 0; + background: rgba(0, 0, 0, 0.68); + backdrop-filter: blur(10px); +} + +.dialog-card { + position: relative; + z-index: 1; + display: grid; + width: min(100%, 360px); + gap: 16px; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--panel); + padding: 18px; + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.48); +} + +.dialog-head { + display: grid; + grid-template-columns: minmax(0, 1fr) 42px; + align-items: start; + gap: 12px; +} + +.dialog-head h2 { + margin: 0; + font-size: 17px; +} + +.dialog-head p { + margin: 6px 0 0; + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.dialog-actions { + display: grid; + gap: 8px; +} + +.toast { + position: fixed; + right: 16px; + bottom: 16px; + z-index: 60; + max-width: min(420px, calc(100vw - 32px)); + border: 1px solid color-mix(in srgb, var(--accent) 46%, var(--border)); + border-radius: var(--radius); + background: rgba(15, 21, 27, 0.92); + box-shadow: 0 16px 42px rgba(0, 0, 0, 0.42); + color: var(--text); + padding: 12px 14px; + font-size: 13px; + font-weight: 800; +} + +.preview-board { + display: grid; + width: max-content; + min-height: 100vh; + grid-template-columns: repeat(3, auto); + gap: 22px 38px; + align-items: start; + padding: 18px 74px 28px; + background: linear-gradient(180deg, #06100c 0%, #02060b 32%, #010407 100%); +} + +.preview-phone-wrap { + display: grid; + gap: 9px; +} + +.preview-phone-wrap h2 { + margin: 0; + text-align: center; + font-size: 18px; + line-height: 1.1; +} + +.preview-phone-wrap h2 span { + color: var(--accent); +} + +.preview-phone { + position: relative; + display: grid; + width: 430px; + min-height: 318px; + align-content: start; + gap: 8px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.22); + border-radius: 18px; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.015)), + #071017; + box-shadow: + 0 28px 80px rgba(0, 0, 0, 0.54), + inset 0 1px 0 rgba(255, 255, 255, 0.08); + padding: 15px 16px 16px; +} + +.preview-phone-wrap.wide .preview-phone { + width: 510px; +} + +.preview-phone .bottom-nav { + right: 16px; + bottom: 8px; + left: 16px; + min-height: 50px; + box-shadow: none; +} + +.preview-phone .bottom-nav span { + font-size: 9px; +} + +.preview-phone .card { + padding: 12px; +} + +.preview-phone .status-card { + padding: 14px; +} + +.preview-phone .btn { + min-height: 38px; + font-size: 12px; + padding: 8px 10px; +} + +.preview-phone .btn-square { + width: 36px; + min-width: 36px; + height: 36px; + padding: 0; +} + +.preview-phone .period-card { + min-height: 72px; +} + +.preview-phone .method-card { + min-height: 42px; +} + +.preview-phone .settings-row { + min-height: 39px; +} + +.preview-phone .settings-avatar { + width: 46px; + height: 46px; +} + +.preview-phone .settings-profile-meta strong { + font-size: 12px; +} + +.preview-phone .settings-profile-meta small { + font-size: 10px; +} + +.preview-phone .brand-mark { + width: 34px; + height: 34px; + font-size: 23px; +} + +.preview-phone .brand-mark-lg { + width: 56px; + height: 56px; + font-size: 39px; +} + +.preview-phone .brand-mark-xl { + width: 78px; + height: 78px; + font-size: 56px; +} + +.preview-phone .home-layout { + min-height: 300px; + gap: 12px; + padding-bottom: 58px; +} + +.preview-phone .home-brand { + padding-top: 0; +} + +.preview-phone .login-brand h1 { + font-size: 24px; +} + +.preview-modal { + position: absolute; + right: 18px; + bottom: 18px; + display: grid; + width: 178px; + gap: 8px; + justify-items: center; + border: 1px solid var(--border-strong); + border-radius: 14px; + background: rgba(20, 29, 35, 0.94); + box-shadow: 0 18px 52px rgba(0, 0, 0, 0.46); + padding: 16px; + text-align: center; +} + +.preview-modal svg { + color: var(--accent); +} + +.preview-modal strong { + font-size: 13px; +} + +.preview-modal p { + margin: 0; + color: var(--muted); + font-size: 10px; + line-height: 1.35; +} + +@media (max-width: 460px) { + .phone-screen { + width: 100vw; + padding-right: 10px; + padding-left: 10px; + } + + .period-grid, + .method-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .copy-row, + .email-row { + grid-template-columns: 1fr; + } +} + +@supports not (color: color-mix(in srgb, #000 50%, #fff)) { + .btn-primary { + background: var(--accent); + } +} +/*$vite$:1*/ \ No newline at end of file diff --git a/bot/app/web/templates/subscription_webapp.html b/bot/app/web/templates/subscription_webapp.html index 5a37e87..bfededb 100644 --- a/bot/app/web/templates/subscription_webapp.html +++ b/bot/app/web/templates/subscription_webapp.html @@ -5,292 +5,25 @@ - + - Моя подписка + /minishop - - + -
Загрузка...
- -
-
-
- -
Моя подписка
-
-
-
- - -
-
- - -
-
-
- -
-
- - ... -
- -
-
- ... - -
-
- -
-
- ... -
- -
- -
- -
- -
-
- - - -
- - -
- - - - - - -
- - - - - - - - - - - - - - +
+ + + + diff --git a/bot/app/web/templates/subscription_webapp.js b/bot/app/web/templates/subscription_webapp.js index 9626b2b..089812b 100644 --- a/bot/app/web/templates/subscription_webapp.js +++ b/bot/app/web/templates/subscription_webapp.js @@ -1,2659 +1,14138 @@ -/* WEBAPP_DEV_MOCK_START */ -window.__WEBAPP_DEV_MOCK__ = { - config: { - title: 'Моя подписка', - primaryColor: '#00fe7a', - logoUrl: '', - apiBase: '/api', - supportUrl: 'https://t.me/support', - privacyPolicyUrl: 'https://example.com/privacy', - userAgreementUrl: 'https://example.com/agreement', - currency: 'RUB', - language: 'ru', - emailAuthEnabled: true, - telegramLoginBotUsername: 'preview_bot' - }, - data: { - ok: true, - user: { - id: 100200300, - username: 'preview', - email: '', - email_verified: false, - telegram_id: 100200300, - telegram_linked: true, - first_name: 'Preview', - language_code: 'ru' - }, - subscription: { - active: true, - status: 'ACTIVE', - remaining_text: '27 д. 5 ч.', - end_date_text: '19.05.2026 22:40', - days_left: 27, - config_link: 'https://sub.example.com/sub/preview-token', - connect_url: 'https://sub.example.com/connect/preview-token', - traffic_used: '18.42 GB', - traffic_limit: '500.00 GB', - traffic_used_bytes: 19778468741, - traffic_limit_bytes: 536870912000, - auto_renew_enabled: false, - provider: null - }, - plans: [ - {months: 1, price: 190, currency: 'RUB', title: '1 месяц'}, - {months: 3, price: 550, currency: 'RUB', title: '3 месяца'}, - {months: 6, price: 1100, currency: 'RUB', title: '6 месяцев'}, - {months: 12, price: 2000, currency: 'RUB', title: '12 месяцев'} - ], - payment_methods: [ - {id: 'cryptopay', name: 'CryptoPay'}, - {id: 'platega_sbp', name: 'Platega · СБП'}, - {id: 'platega_crypto', name: 'Platega · Crypto'}, - {id: 'freekassa', name: 'FreeKassa / СБП'} - ], - referral: { - code: 'AB12CD34E', - bot_link: 'https://t.me/preview_bot?start=ref_uAB12CD34E', - webapp_link: 'https://app.example.com/?ref=uAB12CD34E', - invited_count: 3, - purchased_count: 1, - bonus_details: [ - {months: 1, title: '1 месяц', inviter_days: 3, friend_days: 3}, - {months: 3, title: '3 месяца', inviter_days: 10, friend_days: 7} - ] - }, - settings: { - support_url: 'https://t.me/support', - traffic_mode: false, - email_auth_enabled: true - } - } - }; -/* WEBAPP_DEV_MOCK_END */ - -const MOCK = (() => { - const mock = window.__WEBAPP_DEV_MOCK__; - const host = window.location.hostname; - const isLocal = window.location.protocol === 'file:' || host === 'localhost' || host === '127.0.0.1' || host === ''; - return mock && isLocal ? mock : null; - })(); - function readJsonScript(id) { - const node = document.getElementById(id); - if (!node || !node.textContent) return null; - try { - return JSON.parse(node.textContent); - } catch (error) { - console.warn('Failed to parse JSON config from #' + id, error); - return null; - } - } - - const CFG = readJsonScript('webapp-config') || (MOCK && MOCK.config) || {}; - const tg = window.Telegram && window.Telegram.WebApp ? window.Telegram.WebApp : null; - const TELEGRAM_LOGIN_WIDGET_URL = 'https://telegram.org/js/telegram-widget.js?23'; - const TELEGRAM_LOGIN_WIDGET_RENDER_TIMEOUT_MS = 8000; - const MANUAL_LOGOUT_FLAG_KEY = 'rw_webapp_manual_logout'; - const state = { - token: MOCK ? 'local-preview' : (localStorage.getItem('rw_webapp_token') || ''), - csrfToken: MOCK ? '' : (readCookie('rw_webapp_csrf') || ''), - data: null, - selectedPlan: null, - referralParam: readReferralParam(), - promoApplying: false, - payment: null, - paymentFlowOpen: false, - promoModalOpen: false, - referralModalOpen: false, - accountMergeModalOpen: false, - accountMergeNotice: null, - creatingPayment: false, - authInProgress: false, - authMode: (CFG.emailAuthEnabled === false ? 'telegram' : 'email'), - emailLoginPending: false, - emailLoginEmail: '', - emailLoginCodeModalOpen: false, - emailLoginVerifying: false, - emailLoginResending: false, - emailLinkPending: false, - emailLinkEmail: '', - promoStatusTimer: null, - promoStatusType: '', - promoStatusText: '', - telegramLinkRendered: false, - telegramLinkInProgress: false, - toastTimer: null, - userMenuOpen: false, - userMenuCloseTimer: null, - langOverride: (function() { - try { return localStorage.getItem('rw_webapp_lang') || ''; } catch (e) { return ''; } - })(), - langMenuOpen: false, - langMenuCloseTimer: null - }; - - const LANG_OPTIONS = [ - { code: 'ru', label: 'Русский', short: 'RU', flag: '🇷🇺' }, - { code: 'en', label: 'English', short: 'EN', flag: '🇬🇧' } - ]; - - const TW = { - iconBtn: 'icon-btn', - btnBase: 'btn', - btnIconOnly: 'w-12 min-w-12 p-0', - panelHead: 'panel-head', - flowCaption: 'section-caption', - sectionTitle: 'section-title', - metricLabel: 'metric-label', - metricValue: 'metric-value', - referralLinkRow: 'referral-link-row', - referralLinkValue: 'referral-link-value', - bonusRow: 'bonus-row', - empty: 'empty-state', - planCard: 'plan-card', - planCardActive: 'plan-card-active', - planName: 'plan-name', - planMeta: 'plan-meta', - planPrice: 'plan-price', - paymentMethodCard: 'payment-method-card', - paymentMethodCardPlatega: 'payment-method-card--platega', - paymentMethodCardPlategaSbp: 'payment-method-card--platega payment-method-card--platega-sbp', - paymentMethodCardPlategaCrypto: 'payment-method-card--platega payment-method-card--platega-crypto', - paymentMethodCardCryptopay: 'payment-method-card--cryptopay', - notice: 'notice', - stepNum: 'step-num', - stepName: 'step-name' - }; - - const FALLBACK_I18N = { - ru: { - page_title: 'Моя подписка', - loading: 'Загрузка...', - refresh: 'Обновить', - logout: 'Выйти', - subscription: 'Подписка', - subscription_subtitle: 'Ваша ссылка подключения и срок доступа', - ends_at: 'Окончание', - traffic: 'Трафик', - connect: 'Подключиться', - copy_link: 'Скопировать ссылку', - extend_subscription: 'Купить подписку / Добавить дни', - payment_title: 'Оплата подписки', - payment_caption: 'Выберите период и нажмите способ оплаты. Сервис оплаты откроется сразу.', - close: 'Закрыть', - close_payment: 'Закрыть оплату', - payment_steps: 'Шаги оплаты', - step_plan: 'Срок', - step_method: 'Метод', - step_result: 'Платеж', - select_period: 'Выберите период', - select_method: 'Выберите способ оплаты', - payment_status: 'Статус платежа', - payment_amount_label: 'Стоимость выбранного периода', - choose_payment_method: 'Способ оплаты', - back: 'Назад', - create_payment: 'Создать платеж', - open_payment: 'Открыть оплату', - check_payment: 'Проверить оплату', - choose_other_method: 'Выбрать другой способ', - pay_with_platega_button: 'Оплатить картой (СБП)', - pay_with_platega_sbp_button: 'Оплата через СБП', - pay_with_platega_crypto_button: 'Оплата криптой', - pay_with_cryptopay_button: 'Оплатить криптой', - support: 'Поддержка', - account_title: 'Аккаунт', - account_caption: 'Способы входа', - email_label: 'Email', - telegram_label: 'Telegram', - linked: 'Привязан', - not_linked: 'Не привязан', - email_login_tab: 'Email', - telegram_login_tab: 'Telegram', - email_placeholder: 'mail@example.com', - code_placeholder: '000000', - send_code: 'Отправить код', - resend_code: 'Отправить еще раз', - confirm: 'Подтвердить', - login: 'Войти', - login_title: 'Войдите или зарегистрируйтесь', - login_continue: 'Продолжить', - email_code_title: 'Подтвердите вход', - email_code_caption: 'Введите 6-значный код из письма.', - email_code_aria: 'Код подтверждения', - email_auth_disabled: 'Вход по email пока не настроен.', - email_required: 'Введите email', - email_invalid: 'Введите корректный email', - email_code_sending: 'Отправляю код...', - email_code_sent: 'Код отправлен на почту', - email_code_send_failed: 'Не удалось отправить код', - email_code_invalid: 'Неверный код', - email_code_expired: 'Код устарел', - email_rate_limited: 'Повторная отправка доступна через {seconds} сек.', - email_linked: 'Email привязан', - telegram_linked: 'Telegram привязан', - account_merge_toast: 'Аккаунты объединены', - account_merge_title: 'Аккаунты объединены', - account_merge_caption: 'Оплаченные периоды сложились, и теперь у вас один аккаунт.', - account_merge_body: 'Мы объединили ваш Telegram и email в один профиль. Оплаченные периоды сложились, ссылка на подписку осталась прежней, а более поздний аккаунт был удалён автоматически.', - account_merge_primary_account_label: 'Оставлен аккаунт', - account_merge_removed_account_label: 'Удалён аккаунт', - account_merge_end_date_label: 'Новая дата окончания', - account_merge_conflict: 'Этот аккаунт уже связан с другими данными.', - telegram_auth: 'Telegram auth', - telegram_auth_verifying: 'Проверяю вход...', - telegram_auth_failed: 'Не удалось подтвердить Telegram-вход. Попробуйте еще раз.', - telegram_auth_unavailable: 'Сервер Telegram сейчас недоступен. Попробуйте позже или войдите по email.', - telegram_bot_unavailable: 'Telegram Login Widget недоступен. Попробуйте позже или войдите по email.', - telegram_auth_access_denied: 'Доступ запрещен.', - active: 'Активна', - inactive: 'Не активна', - subscription_active: 'Подписка активна', - subscription_inactive: 'Подписка не активна', - sub_remaining_template: 'Действует еще {value}', - sub_ends_template: 'Закончится {date}', - sub_ended_template: 'Закончилась {date}', - sub_no_expiry: 'Бессрочно', - traffic_unlimited: '∞ бесконечный трафик', - traffic_unavailable: 'Сервис недоступен', - link_panel_title: 'Привяжите способ входа', - link_panel_caption: 'Чтобы не потерять доступ к кабинету', - link_email_title: 'Привяжите email', - link_email_caption: 'Получайте код для входа на почту', - link_telegram_title: 'Привяжите Telegram', - link_telegram_caption: 'Входите одним нажатием через Telegram', - guest_user: 'Гость', - no_active_subscription: 'Нет активной подписки', - not_available: 'N/A', - no_plans: 'Тарифы не настроены.', - access_period: 'Срок доступа', - choose_plan_first: 'Сначала выберите срок подписки.', - no_methods: 'Нет доступных способов оплаты.', - payment_not_created: 'Платеж еще не создан.', - invoice_sent: 'Счет отправлен в чат с ботом. После оплаты вернитесь сюда и проверьте статус.', - payment_created: 'Платеж создан. Откройте оплату, завершите ее у провайдера и затем проверьте статус.', - updating: 'Обновляю данные', - choose_plan_toast: 'Выберите срок подписки', - choose_method_toast: 'Выберите способ оплаты', - create_payment_toast: 'Создайте платеж', - payment_confirmed: 'Оплата подтверждена', - payment_pending: 'Платеж пока не подтвержден', - link_copied: 'Ссылка скопирована', - no_link: 'Ссылка пока недоступна', - payment_error: 'Ошибка оплаты', - privacy_policy: 'Политика конфиденциальности', - user_agreement: 'Пользовательское соглашение', - promo_title: 'Промокод', - promo_caption: 'Введите код, чтобы начислить бонусные дни.', - promo_placeholder: 'PROMO2026', - apply_promo: 'Применить', - promo_applied: 'Промокод применен', - promo_applied_ok: 'Промокод применен, всё ок.', - promo_empty: 'Введите промокод', - promo_invalid: 'Промокод не найден или недействителен.', - referral_title: 'Пригласить друга', - referral_caption: 'Поделитесь ссылкой и получайте бонусы.', - referral_code: 'Ваш код', - invited_friends: 'Приглашено', - purchased_friends: 'Оплатили', - copy_code: 'Скопировать код', - copy_webapp_link: 'Скопировать ссылку Web App', - copy_bot_link: 'Скопировать ссылку бота', - referral_copied: 'Ссылка скопирована', - code_copied: 'Код скопирован', - referral_site_label: 'Пригласить через сайт', - referral_telegram_label: 'Пригласить через телеграм', - referral_bonus_title: 'Бонусы', - referral_bonus_pair: 'Вы: {inviter} дн. / друг: {friend} дн.', - referral_no_bonuses: 'Бонусы пока не настроены.', - referral_bonus_explanation: 'Бонусы начисляются один раз за каждого приглашенного пользователя, когда приглашенный пользователь покупает подписку.' - }, - en: { - page_title: 'My subscription', - loading: 'Loading...', - refresh: 'Refresh', - logout: 'Log out', - subscription: 'Subscription', - subscription_subtitle: 'Your connection link and access time', - ends_at: 'Ends at', - traffic: 'Traffic', - connect: 'Connect', - copy_link: 'Copy link', - extend_subscription: 'Renew subscription/Add days', - payment_title: 'Subscription payment', - payment_caption: 'Choose a period and tap a payment method. The payment service will open right away.', - close: 'Close', - close_payment: 'Close payment', - payment_steps: 'Payment steps', - step_plan: 'Period', - step_method: 'Method', - step_result: 'Payment', - select_period: 'Select period', - select_method: 'Select payment method', - payment_status: 'Payment status', - payment_amount_label: 'Selected period cost', - choose_payment_method: 'Payment method', - back: 'Back', - create_payment: 'Create payment', - open_payment: 'Open payment', - check_payment: 'Check payment', - choose_other_method: 'Choose another method', - pay_with_platega_button: 'Pay by card (SBP)', - pay_with_platega_sbp_button: 'Pay via SBP', - pay_with_platega_crypto_button: 'Pay with crypto', - pay_with_cryptopay_button: 'Pay with crypto', - support: 'Support', - account_title: 'Account', - account_caption: 'Sign-in methods', - email_label: 'Email', - telegram_label: 'Telegram', - linked: 'Linked', - not_linked: 'Not linked', - email_login_tab: 'Email', - telegram_login_tab: 'Telegram', - email_placeholder: 'mail@example.com', - code_placeholder: '000000', - send_code: 'Send code', - resend_code: 'Send again', - confirm: 'Confirm', - login: 'Log in', - login_title: 'Log in or sign up', - login_continue: 'Continue', - email_code_title: 'Confirm login', - email_code_caption: 'Enter the 6-digit code from the email.', - email_code_aria: 'Verification code', - email_auth_disabled: 'Email sign-in is not configured yet.', - email_required: 'Enter your email', - email_invalid: 'Enter a valid email address', - email_code_sending: 'Sending code...', - email_code_sent: 'Code sent to email', - email_code_send_failed: 'Could not send code', - email_code_invalid: 'Invalid code', - email_code_expired: 'Code expired', - email_rate_limited: 'Try again in {seconds} sec.', - email_linked: 'Email linked', - telegram_linked: 'Telegram linked', - account_merge_toast: 'Accounts merged', - account_merge_title: 'Accounts merged', - account_merge_caption: 'Your paid periods were combined and you now have one account.', - account_merge_body: 'We merged your Telegram and email accounts into one profile. Your paid periods were combined, your subscription link stayed the same, and the later account was removed automatically.', - account_merge_primary_account_label: 'Kept account', - account_merge_removed_account_label: 'Removed account', - account_merge_end_date_label: 'New end date', - account_merge_conflict: 'This account is already linked to different data.', - telegram_auth: 'Telegram auth', - telegram_auth_verifying: 'Verifying login...', - telegram_auth_failed: 'Could not verify Telegram login. Try again.', - telegram_auth_unavailable: 'Telegram server is unavailable right now. Try again later or sign in by email.', - telegram_bot_unavailable: 'Telegram Login Widget is unavailable. Try again later or sign in by email.', - telegram_auth_access_denied: 'Access denied.', - active: 'Active', - inactive: 'Inactive', - subscription_active: 'Subscription is active', - subscription_inactive: 'Subscription is inactive', - sub_remaining_template: 'Valid for {value} more', - sub_ends_template: 'Expires {date}', - sub_ended_template: 'Expired {date}', - sub_no_expiry: 'No expiration', - traffic_unlimited: '∞ unlimited traffic', - traffic_unavailable: 'Service unavailable', - link_panel_title: 'Link a sign-in method', - link_panel_caption: 'So you never lose access', - link_email_title: 'Link email', - link_email_caption: 'Receive sign-in codes by email', - link_telegram_title: 'Link Telegram', - link_telegram_caption: 'Sign in with one tap via Telegram', - guest_user: 'Guest', - no_active_subscription: 'No active subscription', - not_available: 'N/A', - no_plans: 'Plans are not configured.', - access_period: 'Access period', - choose_plan_first: 'Choose a subscription period first.', - no_methods: 'No payment methods available.', - payment_not_created: 'Payment has not been created yet.', - invoice_sent: 'The invoice was sent to the bot chat. Pay it, then return here and check the status.', - payment_created: 'Payment was created. Open it, complete payment with the provider, then check the status.', - updating: 'Refreshing data', - choose_plan_toast: 'Choose a subscription period', - choose_method_toast: 'Choose a payment method', - create_payment_toast: 'Create a payment', - payment_confirmed: 'Payment confirmed', - payment_pending: 'Payment is not confirmed yet', - link_copied: 'Link copied', - no_link: 'Link is not available yet', - payment_error: 'Payment error', - privacy_policy: 'Privacy policy', - user_agreement: 'User agreement', - promo_title: 'Promo code', - promo_caption: 'Enter a code to add bonus days.', - promo_placeholder: 'PROMO2026', - apply_promo: 'Apply', - promo_applied: 'Promo code applied', - promo_applied_ok: 'Promo code applied, all good.', - promo_empty: 'Enter promo code', - promo_invalid: 'Promo code was not found or is not valid.', - referral_title: 'Invite friend', - referral_caption: 'Share your link and receive bonuses.', - referral_code: 'Your code', - invited_friends: 'Invited', - purchased_friends: 'Purchased', - copy_code: 'Copy code', - copy_webapp_link: 'Copy Web App link', - copy_bot_link: 'Copy bot link', - referral_copied: 'Link copied', - code_copied: 'Code copied', - referral_site_label: 'Invite via site', - referral_telegram_label: 'Invite via Telegram', - referral_bonus_title: 'Bonuses', - referral_bonus_pair: 'You: {inviter} d. / friend: {friend} d.', - referral_no_bonuses: 'Bonuses are not configured yet.', - referral_bonus_explanation: 'Bonuses are awarded once for each invited user when they purchase a subscription.' - } - }; - const I18N = (function () { - const server = readJsonScript('i18n') || (MOCK && MOCK.i18n) || {}; - const merged = {}; - const langs = new Set(Object.keys(FALLBACK_I18N).concat(Object.keys(server))); - langs.forEach(function (lang) { - merged[lang] = Object.assign({}, FALLBACK_I18N[lang] || {}, server[lang] || {}); - }); - return merged; - })(); - - const accent = CFG.primaryColor || '#00fe7a'; - document.documentElement.style.setProperty('--accent', accent); - setBrand(); - applyI18n(); - applyLegalLinks(); - if (CFG.supportUrl) { - const support = document.getElementById('support-link'); - support.href = CFG.supportUrl; - support.classList.remove('hidden'); - } - - if (tg) { - try { - tg.ready(); - tg.expand(); - const canSetColors = typeof tg.isVersionAtLeast === 'function' && tg.isVersionAtLeast('6.1'); - if (canSetColors) { - tg.setHeaderColor('#05070a'); - tg.setBackgroundColor('#05070a'); - } - } catch (e) { } - } - - bindEmailLoginInput(); - bindEmailCodeInput(); - bindPromoInput(); - - document.addEventListener('keydown', event => { - handleDocumentKeyForUserMenu(event); - if (event.key !== 'Escape') return; - if (state.emailLoginCodeModalOpen) { - closeEmailLoginCodeModal(); - } else if (state.accountMergeModalOpen) { - closeAccountMergeModal(); - } else if (state.promoModalOpen) { - closePromoModal(); - } else if (state.referralModalOpen) { - closeReferralModal(); - } else if (state.paymentFlowOpen) { - closePaymentFlow(); - } - }); - - document.addEventListener('click', handleDocumentActionClick); - document.addEventListener('click', handleDocumentClickForUserMenu); - document.addEventListener('click', handleDocumentClickForLangMenu); - renderLangMenu(); - - boot(); - - async function boot() { - showLoader(); - if (MOCK) { - await loadData(); - return; - } - - const magicToken = readMagicLoginToken(); - if (magicToken) { - const authenticated = await finalizeMagicLogin(magicToken); - if (authenticated) { - return; - } - } - - // Explicit logout should survive refresh, even if the old cookie session is still valid. - if (isManuallyLoggedOut()) { - await startExternalAuth(); - return; - } - - const widgetAuthData = readTelegramLoginWidgetAuthData(); - if (widgetAuthData) { - const authenticated = await finalizeTelegramAuth(widgetAuthData); - if (authenticated) { - return; - } - clearToken(); - startExternalAuth({resetStatus: false}); - return; - } - - if (tg && tg.initData) { - try { - const authenticated = await finalizeTelegramAuth(tg.initData, 'init_data'); - if (authenticated) { - return; - } - } catch (e) { } - } - - if (state.token || readCookie('rw_webapp_csrf')) { - try { - await loadData(); - return; - } catch (e) { - clearToken(); - } - } - - await startExternalAuth(); - } - - function readMagicLoginToken() { - const query = new URLSearchParams(window.location.search); - const token = (query.get('login_token') || '').trim(); - return token || null; - } - - function clearMagicLoginQuery() { - const url = new URL(window.location.href); - ['login_token', 'login_purpose'].forEach(key => url.searchParams.delete(key)); - if (window.history && window.history.replaceState) { - window.history.replaceState({}, document.title, url.pathname + url.search + url.hash); - } - } - - async function finalizeMagicLogin(token) { - if (state.authInProgress) return false; - state.authInProgress = true; - setAuthStatus(t('telegram_auth_verifying')); - try { - const payload = {token}; - if (state.referralParam) payload.referral_code = state.referralParam; - const data = await publicApi('/auth/email/magic', payload); - if (data && data.ok && data.token) { - setToken(data.token, data.csrf_token); - clearManualLogoutFlag(); - clearMagicLoginQuery(); - try { - setAuthStatus(''); - await loadData(); - return true; - } catch (e) { - clearToken(); - setAuthStatus(t('telegram_auth_failed'), true); - return false; - } - } - clearMagicLoginQuery(); - setAuthStatus(emailErrorMessage(data, 'email_code_invalid'), true); - return false; - } catch (e) { - clearMagicLoginQuery(); - setAuthStatus(t('telegram_auth_failed'), true); - return false; - } finally { - state.authInProgress = false; - } - } - - function readTelegramLoginWidgetAuthData() { - const query = new URLSearchParams(window.location.search); - const keys = ['id', 'first_name', 'last_name', 'username', 'photo_url', 'auth_date', 'hash']; - const authData = {}; - let hasAuthValue = false; - - keys.forEach(key => { - if (!query.has(key)) return; - authData[key] = query.get(key) || ''; - hasAuthValue = true; - }); - - if (!hasAuthValue || !authData.id || !authData.auth_date || !authData.hash) { - return null; - } - - return authData; - } - - function readReferralParam() { - const query = new URLSearchParams(window.location.search); - const fromQuery = query.get('ref') || query.get('start') || query.get('start_param') || ''; - const fromTelegram = tg && tg.initDataUnsafe && tg.initDataUnsafe.start_param - ? tg.initDataUnsafe.start_param - : ''; - const value = String(fromTelegram || fromQuery || '').trim(); - if (value) { - localStorage.setItem('rw_webapp_referral', value); - return value; - } - return localStorage.getItem('rw_webapp_referral') || ''; - } - - function clearTelegramLoginWidgetQuery() { - const url = new URL(window.location.href); - const keys = ['id', 'first_name', 'last_name', 'username', 'photo_url', 'auth_date', 'hash']; - keys.forEach(key => url.searchParams.delete(key)); - if (window.history && window.history.replaceState) { - window.history.replaceState({}, document.title, url.pathname + url.search + url.hash); - } - } - - async function finalizeTelegramAuth(authData, source = 'auth_data') { - if (state.authInProgress) return false; - state.authInProgress = true; - setAuthStatus(t('telegram_auth_verifying')); - try { - const payload = source === 'init_data' - ? {init_data: authData} - : {auth_data: authData}; - if (state.referralParam) payload.referral_code = state.referralParam; - const response = await fetch(CFG.apiBase + '/auth/token', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(payload) - }); - const data = await response.json(); - if (response.ok && data.ok && data.token) { - setToken(data.token, data.csrf_token); - clearTelegramLoginWidgetQuery(); - try { - setAuthStatus(''); - await loadData(); - return true; - } catch (e) { - clearToken(); - setAuthStatus(t('telegram_auth_failed'), true); - return false; - } - } - - const errorKey = data.error === 'banned' || data.error === 'access_denied' - ? 'telegram_auth_access_denied' - : 'telegram_auth_failed'; - setAuthStatus(t(errorKey), true); - } catch (e) { - setAuthStatus(t('telegram_auth_failed'), true); - } finally { - state.authInProgress = false; - } - - clearTelegramLoginWidgetQuery(); - return false; - } - - async function handleTelegramLoginWidgetAuth(user) { - if (!user) { - setAuthStatus(t('telegram_auth_failed'), true); - return; - } - - await finalizeTelegramAuth(user, 'auth_data'); - } - - function appendTelegramLoginWidget(container, botUsername, callbackName, onUnavailable) { - const script = document.createElement('script'); - let unavailableShown = false; - const showUnavailable = () => { - if (unavailableShown) return; - unavailableShown = true; - onUnavailable(); - }; - - script.async = true; - script.src = TELEGRAM_LOGIN_WIDGET_URL; - script.setAttribute('data-telegram-login', botUsername); - script.setAttribute('data-size', 'large'); - script.setAttribute('data-userpic', 'true'); - script.setAttribute('data-request-access', 'write'); - script.setAttribute('data-onauth', `${callbackName}(user)`); - script.onerror = showUnavailable; - script.onload = () => { - window.setTimeout(() => { - if (!container.contains(script) || container.querySelector('iframe')) return; - showUnavailable(); - }, TELEGRAM_LOGIN_WIDGET_RENDER_TIMEOUT_MS); - }; - container.appendChild(script); - } - - function setAuthMode(mode) { - state.authMode = mode === 'telegram' ? 'telegram' : 'email'; - renderAuthMode(); - } - - function renderAuthMode() { - const emailTab = document.getElementById('email-auth-tab'); - const telegramTab = document.getElementById('telegram-auth-tab'); - const emailPane = document.getElementById('email-login-pane'); - const telegramPane = document.getElementById('telegram-login-pane'); - const authBody = document.querySelector('.login-auth-body'); - const emailEnabled = CFG.emailAuthEnabled !== false; - if (!emailEnabled && state.authMode === 'email') { - state.authMode = 'telegram'; - } - - emailTab.classList.toggle('active', state.authMode === 'email'); - telegramTab.classList.toggle('active', state.authMode === 'telegram'); - emailPane.classList.toggle('hidden', state.authMode !== 'email'); - telegramPane.classList.toggle('hidden', state.authMode !== 'telegram'); - if (authBody) { - authBody.classList.toggle('login-auth-body--telegram', state.authMode === 'telegram'); - } - - emailTab.disabled = !emailEnabled; - if (state.authMode === 'telegram') { - renderTelegramLoginWidget(); - } else if (!emailEnabled) { - setAuthStatus(t('email_auth_disabled'), true); - } - } - - function renderTelegramLoginWidget() { - const container = document.getElementById('telegram-login-widget'); - if (!container) return; - - container.innerHTML = ''; - const botUsername = String(CFG.telegramLoginBotUsername || '').trim(); - if (!botUsername) { - setAuthStatus(t('telegram_bot_unavailable'), true); - return; - } - - if (typeof window.onTelegramAuth !== 'function') { - window.onTelegramAuth = async function(user) { - await handleTelegramLoginWidgetAuth(user); - }; - } - - setAuthStatus(''); - appendTelegramLoginWidget( - container, - botUsername, - 'onTelegramAuth', - () => setAuthStatus(t('telegram_auth_unavailable'), true) - ); - } - - function bindEmailLoginInput() { - const input = document.getElementById('email-login-input'); - if (!input) return; - - input.addEventListener('keydown', event => { - if (event.key !== 'Enter') return; - event.preventDefault(); - requestEmailLoginCode(); - }); - input.addEventListener('input', () => { - if (input.getAttribute('aria-invalid') !== 'true') return; - input.removeAttribute('aria-invalid'); - setAuthStatus(''); - }); - } - - function bindEmailCodeInput() { - const input = document.getElementById('email-login-code-input'); - if (!input) return; - - input.addEventListener('input', () => { - input.value = sanitizeCode(input.value); - updateEmailLoginCodeSlots(); - }); - input.addEventListener('focus', updateEmailLoginCodeSlots); - input.addEventListener('blur', updateEmailLoginCodeSlots); - input.addEventListener('keydown', event => { - if (event.key === 'Enter') { - event.preventDefault(); - if (sanitizeCode(input.value).length === 6 && !state.emailLoginVerifying) { - verifyEmailLoginCode(); - } - } - }); - } - - function sanitizeCode(value) { - return String(value || '').replace(/\D/g, '').slice(0, 6); - } - - function isValidEmail(value) { - const email = normalizeEmail(value); - return Boolean(email && email.length <= 254 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)); - } - - function readValidEmailLoginInput() { - const input = document.getElementById('email-login-input'); - const email = normalizeEmail(input && input.value); - if (!email || !isValidEmail(email)) { - if (input) { - input.setAttribute('aria-invalid', 'true'); - input.focus(); - } - setAuthStatus(t(email ? 'email_invalid' : 'email_required'), true); - return ''; - } - - if (input) { - input.value = email; - input.removeAttribute('aria-invalid'); - } - return email; - } - - function openEmailLoginCodeModal(email, message, isError = false) { - state.emailLoginEmail = email; - state.emailLoginCodeModalOpen = true; - const input = document.getElementById('email-login-code-input'); - if (input) input.value = ''; - renderEmailLoginCodeModal(); - setEmailCodeStatus(message || t('email_code_sent'), isError); - window.setTimeout(() => { - const codeInput = document.getElementById('email-login-code-input'); - if (state.emailLoginCodeModalOpen && codeInput) codeInput.focus(); - }, 80); - } - - function closeEmailLoginCodeModal() { - state.emailLoginCodeModalOpen = false; - const input = document.getElementById('email-login-code-input'); - if (input) input.value = ''; - setEmailCodeStatus(''); - renderEmailLoginCodeModal(); - } - - function renderEmailLoginCodeModal() { - const modal = document.getElementById('email-code-modal'); - if (!modal) return; - - if (!state.emailLoginCodeModalOpen) { - modal.classList.remove('show'); - syncModalLock(); - window.setTimeout(() => { - if (!state.emailLoginCodeModalOpen) modal.classList.add('hidden'); - }, 180); - return; - } - - const address = document.getElementById('email-code-address'); - if (address) address.textContent = state.emailLoginEmail || '...'; - modal.classList.remove('hidden'); - syncModalLock(); - applyI18n(modal); - updateEmailLoginCodeSlots(); - window.requestAnimationFrame(() => modal.classList.add('show')); - } - - function updateEmailLoginCodeSlots() { - const input = document.getElementById('email-login-code-input'); - const value = sanitizeCode(input && input.value); - if (input && input.value !== value) input.value = value; - - for (let index = 0; index < 6; index += 1) { - const slot = document.getElementById('email-code-slot-' + index); - if (!slot) continue; - const character = value[index] || ''; - slot.textContent = character; - slot.classList.toggle('filled', Boolean(character)); - slot.classList.toggle( - 'active', - state.emailLoginCodeModalOpen - && document.activeElement === input - && index === Math.min(value.length, 5) - ); - } - - const verifyButton = document.getElementById('email-login-verify-btn'); - if (verifyButton) { - verifyButton.disabled = value.length !== 6 || state.emailLoginVerifying || state.emailLoginPending; - } - - const resendButton = document.getElementById('email-login-resend-btn'); - if (resendButton) { - resendButton.disabled = state.emailLoginPending || state.emailLoginResending; - } - } - - async function requestEmailLoginCode() { - if (state.emailLoginPending) return; - if (CFG.emailAuthEnabled === false) { - setAuthStatus(t('email_auth_disabled'), true); - return; - } - const email = readValidEmailLoginInput(); - if (!email) { - return; - } - state.emailLoginPending = true; - setButtonBusy('email-login-send-btn', true); - openEmailLoginCodeModal(email, t('email_code_sending')); - setAuthStatus(''); - try { - const data = await publicApi('/auth/email/request', { - email, - language: getLanguage() - }); - if (!data.ok) throw data; - setEmailCodeStatus(t('email_code_sent')); - } catch (e) { - const message = emailErrorMessage(e, 'email_code_send_failed'); - if (e && e.error === 'invalid_email') { - closeEmailLoginCodeModal(); - const input = document.getElementById('email-login-input'); - if (input) input.setAttribute('aria-invalid', 'true'); - setAuthStatus(message, true); - } else { - setEmailCodeStatus(message, true); - } - } finally { - state.emailLoginPending = false; - setButtonBusy('email-login-send-btn', false); - updateEmailLoginCodeSlots(); - } - } - - async function resendEmailLoginCode() { - if (state.emailLoginPending || state.emailLoginResending) return; - const email = state.emailLoginEmail || normalizeEmail(document.getElementById('email-login-input').value); - if (!email || !isValidEmail(email)) { - setEmailCodeStatus(t(email ? 'email_invalid' : 'email_required'), true); - return; - } - - state.emailLoginResending = true; - updateEmailLoginCodeSlots(); - setEmailCodeStatus(t('email_code_sending')); - try { - const data = await publicApi('/auth/email/request', { - email, - language: getLanguage() - }); - if (!data.ok) throw data; - state.emailLoginEmail = email; - const input = document.getElementById('email-login-code-input'); - if (input) input.value = ''; - setEmailCodeStatus(t('email_code_sent')); - } catch (e) { - setEmailCodeStatus(emailErrorMessage(e, 'email_code_send_failed'), true); - } finally { - state.emailLoginResending = false; - updateEmailLoginCodeSlots(); - } - } - - async function verifyEmailLoginCode() { - const email = state.emailLoginEmail || normalizeEmail(document.getElementById('email-login-input').value); - const input = document.getElementById('email-login-code-input'); - const code = sanitizeCode(input && input.value); - if (!email || code.length !== 6) { - setEmailCodeStatus(t('email_code_invalid'), true); - updateEmailLoginCodeSlots(); - return; - } - - state.emailLoginVerifying = true; - updateEmailLoginCodeSlots(); - setEmailCodeStatus(t('telegram_auth_verifying')); - try { - const data = await publicApi('/auth/email/verify', { - email, - code, - referral_code: state.referralParam || '' - }); - if (!data.ok || !data.token) throw data; - setToken(data.token, data.csrf_token); - closeEmailLoginCodeModal(); - await loadData(); - } catch (e) { - clearToken(); - setEmailCodeStatus(emailErrorMessage(e, 'email_code_invalid'), true); - } finally { - state.emailLoginVerifying = false; - updateEmailLoginCodeSlots(); - } - } - - function startExternalAuth(options = {}) { - const resetStatus = options.resetStatus !== false; - showLogin(); - if (resetStatus) { - setAuthStatus(''); - } - renderAuthMode(); - } - - async function loadData() { - const data = await api('/me'); - if (!data.ok) throw new Error(data.error || 'load failed'); - state.data = data; - setBrand(); - applyI18n(); - applyLegalLinks(); - const plans = data.plans || []; - state.selectedPlan = getDefaultPaymentPlan(plans); - state.payment = null; - state.creatingPayment = false; - state.telegramLinkRendered = false; - render(); - showApp(); - } - - function render() { - renderSubscription(state.data.subscription); - renderUserMenu(state.data.user || {}); - renderAccount(state.data.user || {}); - renderReferral(state.data.referral || {}); - renderAccountMergeModal(); - renderPaymentFlow(); - } - - function renderSubscription(sub) { - const wrap = document.getElementById('sub-status'); - const text = document.getElementById('sub-status-text'); - const remaining = document.getElementById('remaining'); - const endCaption = document.getElementById('end-date-caption'); - - wrap.classList.toggle('active', Boolean(sub.active)); - text.textContent = sub.active ? t('subscription_active') : t('subscription_inactive'); - - if (sub.active) { - const compact = formatCompactRemaining(sub.days_left, getLanguage()) || sub.remaining_text || ''; - remaining.textContent = compact; - } else { - remaining.textContent = t('no_active_subscription'); - } - - if (sub.end_date_text) { - const template = sub.active ? 'sub_ends_template' : 'sub_ended_template'; - endCaption.textContent = t(template, {date: sub.end_date_text}); - endCaption.classList.remove('hidden'); - } else if (sub.active) { - endCaption.textContent = t('sub_no_expiry'); - endCaption.classList.remove('hidden'); - } else { - endCaption.textContent = ''; - endCaption.classList.add('hidden'); - } - - renderTraffic(sub); - document.getElementById('connect-actions').classList.toggle('hidden', !sub.connect_url && !sub.config_link); - } - - function renderTraffic(sub) { - const bar = document.getElementById('traffic-bar'); - const fill = document.getElementById('traffic-bar-fill'); - const value = document.getElementById('traffic'); - if (!bar || !fill || !value) return; - - const limitBytes = Number(sub.traffic_limit_bytes); - const usedBytes = Number(sub.traffic_used_bytes); - const hasLimit = Number.isFinite(limitBytes) && limitBytes > 0; - const hasUsed = Number.isFinite(usedBytes) && usedBytes >= 0; - const usedText = sub.traffic_used || t('not_available'); - const limitText = sub.traffic_limit || t('not_available'); - - bar.classList.remove('over', 'unlimited', 'inactive'); - - if (!sub.active) { - bar.classList.add('inactive'); - fill.style.width = '100%'; - value.textContent = t('traffic_unavailable'); - bar.setAttribute('aria-valuenow', '0'); - return; - } - - if (!hasLimit) { - bar.classList.add('unlimited'); - fill.style.width = '100%'; - value.textContent = t('traffic_unlimited'); - bar.setAttribute('aria-valuenow', '0'); - return; - } - - const ratio = hasUsed ? usedBytes / limitBytes : 0; - const pct = Math.max(0, Math.min(100, Math.round(ratio * 100))); - fill.style.width = pct + '%'; - value.textContent = usedText + ' / ' + limitText; - bar.setAttribute('aria-valuenow', String(pct)); - if (hasUsed && usedBytes >= limitBytes) { - bar.classList.add('over'); - } - } - - function formatCompactRemaining(daysRaw, lang) { - const days = Math.max(0, Math.floor(Number(daysRaw) || 0)); - if (days <= 0) return null; - if (days >= 365) { - const years = Math.round((days / 365) * 2) / 2; - return formatDecimal(years, lang) + ' ' + pluralizeUnit(years, 'year', lang); - } - if (days >= 30) { - const months = Math.round((days / 30) * 2) / 2; - return formatDecimal(months, lang) + ' ' + pluralizeUnit(months, 'month', lang); - } - return formatDecimal(days, lang) + ' ' + pluralizeUnit(days, 'day', lang); - } - - function formatDecimal(value, lang) { - if (Number.isInteger(value)) return String(value); - const str = value.toFixed(1); - return lang === 'ru' ? str.replace('.', ',') : str; - } - - function pluralizeUnit(value, kind, lang) { - const enMap = {year: ['year', 'years'], month: ['month', 'months'], day: ['day', 'days']}; - const ruMap = { - year: ['год', 'года', 'лет'], - month: ['месяц', 'месяца', 'месяцев'], - day: ['день', 'дня', 'дней'] - }; - if (lang === 'en') { - return value === 1 ? enMap[kind][0] : enMap[kind][1]; - } - const forms = ruMap[kind]; - if (!Number.isInteger(value)) return forms[1]; - const n = Math.abs(value); - const mod10 = n % 10; - const mod100 = n % 100; - if (mod10 === 1 && mod100 !== 11) return forms[0]; - if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return forms[1]; - return forms[2]; - } - - function renderUserMenu(user) { - const emailLinked = Boolean(user.email && user.email_verified); - const telegramLinked = Boolean(user.telegram_linked); - const displayName = getUserDisplayName(user); - const secondary = getUserSecondaryText(user, emailLinked, telegramLinked); - const avatarSrc = getUserAvatarSrc(user); - const avatarFallback = buildIdenticon(getUserAvatarSeed(user, emailLinked)); - - const chipName = document.getElementById('user-chip-name'); - const chipAvatar = document.getElementById('user-chip-avatar'); - const dropAvatar = document.getElementById('user-dropdown-avatar'); - const dropName = document.getElementById('user-dropdown-name'); - const dropSub = document.getElementById('user-dropdown-sub'); - const emailStatus = document.getElementById('user-dropdown-email-status'); - const telegramStatus = document.getElementById('user-dropdown-telegram-status'); - - chipName.textContent = displayName; - setAvatarWithFallback(chipAvatar, avatarSrc, avatarFallback, displayName); - setAvatarWithFallback(dropAvatar, avatarSrc, avatarFallback, displayName); - dropName.textContent = displayName; - dropSub.textContent = secondary; - dropSub.classList.toggle('hidden', !secondary); - - emailStatus.textContent = emailLinked ? user.email : t('not_linked'); - emailStatus.classList.toggle('linked', emailLinked); - emailStatus.classList.toggle('unlinked', !emailLinked); - - telegramStatus.textContent = telegramLinked - ? (user.telegram_id ? String(user.telegram_id) : t('linked')) - : t('not_linked'); - telegramStatus.classList.toggle('linked', telegramLinked); - telegramStatus.classList.toggle('unlinked', !telegramLinked); - } - - function renderAccount(user) { - const emailLinked = Boolean(user.email && user.email_verified); - const telegramLinked = Boolean(user.telegram_linked); - const emailAuthEnabled = Boolean(state.data.settings && state.data.settings.email_auth_enabled); - const emailCtaVisible = !emailLinked && emailAuthEnabled; - const telegramCtaVisible = !telegramLinked; - - const emailBox = document.getElementById('email-link-box'); - const telegramBox = document.getElementById('telegram-link-box'); - const linkPanel = document.getElementById('link-panel'); - emailBox.classList.toggle('hidden', !emailCtaVisible); - telegramBox.classList.toggle('hidden', !telegramCtaVisible); - linkPanel.classList.toggle('hidden', !emailCtaVisible && !telegramCtaVisible); - - if (!telegramLinked) { - renderTelegramLinkWidget(); - } else { - state.telegramLinkRendered = false; - } - } - - function getTelegramInitUser() { - if (!tg || !tg.initDataUnsafe || !tg.initDataUnsafe.user) return null; - return tg.initDataUnsafe.user; - } - - function getUserAvatarSeed(user, emailLinked) { - const tgUser = getTelegramInitUser(); - if (emailLinked && user && user.email) return user.email.trim().toLowerCase(); - return String((user && user.id) || (user && user.telegram_id) || (tgUser && tgUser.id) || 'guest'); - } - - function getUserAvatarSrc(user) { - const tgUser = getTelegramInitUser(); - if (tgUser && tgUser.photo_url) return tgUser.photo_url; - if (user && user.telegram_photo_url) return user.telegram_photo_url; - return ''; - } - - function getUserDisplayName(user) { - const tgUser = getTelegramInitUser(); - if (tgUser && tgUser.first_name) return tgUser.first_name; - if (user && user.first_name) return user.first_name; - if (user && user.username) return '@' + user.username; - if (tgUser && tgUser.username) return '@' + tgUser.username; - if (user && user.email) return user.email; - return t('guest_user'); - } - - function getUserSecondaryText(user, emailLinked, telegramLinked) { - if (emailLinked && user.email) return user.email; - if (telegramLinked && user.telegram_id) return '#' + user.telegram_id; - return ''; - } - - function setAvatarWithFallback(img, avatarSrc, avatarFallback, altText) { - if (!img) return; - - img.alt = altText || ''; - if (!avatarSrc) { - img.onerror = null; - img.src = avatarFallback; - return; - } - - img.onerror = () => { - img.onerror = null; - img.src = avatarFallback; - }; - img.src = avatarSrc; - } - - function buildIdenticon(seed) { - const raw = String(seed || 'guest'); - let hash = 2166136261 >>> 0; - for (let i = 0; i < raw.length; i++) { - hash ^= raw.charCodeAt(i); - hash = Math.imul(hash, 16777619) >>> 0; - } - const hue = hash % 360; - const bg = 'hsl(' + hue + ', 60%, 18%)'; - const fg = 'hsl(' + ((hue + 180) % 360) + ', 78%, 62%)'; - let rects = ''; - let bits = hash; - for (let y = 0; y < 5; y++) { - for (let x = 0; x < 3; x++) { - const on = bits & 1; - bits = (bits >>> 1) ^ ((bits & 1) ? 0xB400 : 0); - if (!on) continue; - rects += ''; - if (x !== 2) { - rects += ''; - } - } - } - const svg = '' + - '' + - '' + rects + '' + - ''; - return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg); - } - - function toggleUserMenu(force) { - const open = typeof force === 'boolean' ? force : !state.userMenuOpen; - state.userMenuOpen = open; - const chip = document.getElementById('user-chip'); - const dropdown = document.getElementById('user-dropdown'); - if (!chip || !dropdown) return; - - if (state.userMenuCloseTimer) { - window.clearTimeout(state.userMenuCloseTimer); - state.userMenuCloseTimer = null; - } - - chip.setAttribute('aria-expanded', open ? 'true' : 'false'); - if (open) { - dropdown.classList.remove('hidden'); - dropdown.classList.remove('show'); - window.requestAnimationFrame(() => { - if (state.userMenuOpen) { - dropdown.classList.add('show'); - } - }); - return; - } - - dropdown.classList.remove('show'); - state.userMenuCloseTimer = window.setTimeout(() => { - if (!state.userMenuOpen) { - dropdown.classList.add('hidden'); - } - state.userMenuCloseTimer = null; - }, 180); - } - - function handleDocumentClickForUserMenu(event) { - if (!state.userMenuOpen) return; - const menu = document.querySelector('.user-menu'); - if (menu && menu.contains(event.target)) return; - toggleUserMenu(false); - } - - function handleDocumentKeyForUserMenu(event) { - if (event.key === 'Escape' && state.userMenuOpen) { - toggleUserMenu(false); - } - if (event.key === 'Escape' && state.langMenuOpen) { - toggleLangMenu(false); - } - } - - function renderLangMenu() { - const current = getLanguage(); - const chipLabel = document.getElementById('lang-chip-label'); - const chipFlag = document.getElementById('lang-chip-flag'); - const opt = LANG_OPTIONS.find(o => o.code === current) || LANG_OPTIONS[0]; - if (chipLabel) chipLabel.textContent = opt.short; - if (chipFlag) chipFlag.textContent = opt.flag; - const list = document.getElementById('lang-dropdown'); - if (!list) return; - list.innerHTML = LANG_OPTIONS.map(o => ( - '' - )).join(''); - list.querySelectorAll('[data-lang]').forEach(btn => { - btn.addEventListener('click', () => setLanguage(btn.dataset.lang)); - }); - } - - function toggleLangMenu(force) { - const open = typeof force === 'boolean' ? force : !state.langMenuOpen; - state.langMenuOpen = open; - const chip = document.getElementById('lang-chip'); - const dropdown = document.getElementById('lang-dropdown'); - if (!chip || !dropdown) return; - if (state.langMenuCloseTimer) { - window.clearTimeout(state.langMenuCloseTimer); - state.langMenuCloseTimer = null; - } - chip.setAttribute('aria-expanded', open ? 'true' : 'false'); - if (open) { - dropdown.classList.remove('hidden'); - dropdown.classList.remove('show'); - window.requestAnimationFrame(() => { - if (state.langMenuOpen) dropdown.classList.add('show'); - }); - return; - } - dropdown.classList.remove('show'); - state.langMenuCloseTimer = window.setTimeout(() => { - if (!state.langMenuOpen) dropdown.classList.add('hidden'); - state.langMenuCloseTimer = null; - }, 180); - } - - function handleDocumentClickForLangMenu(event) { - if (!state.langMenuOpen) return; - const menu = document.querySelector('.lang-menu'); - if (menu && menu.contains(event.target)) return; - toggleLangMenu(false); - } - - function handleDocumentActionClick(event) { - const target = event.target instanceof Element ? event.target.closest('[data-action]') : null; - if (!target || !document.contains(target) || target.disabled) return; - - const action = String(target.dataset.action || ''); - if (!action) return; - - switch (action) { - case 'toggle-user-menu': - toggleUserMenu(); - break; - case 'toggle-lang-menu': - toggleLangMenu(); - break; - case 'logout': - logout(); - break; - case 'open-connect-link': - openConnectLink(); - break; - case 'toggle-payment-flow': - togglePaymentFlow(); - break; - case 'open-referral-modal': - openReferralModal(); - break; - case 'open-promo-modal': - openPromoModal(); - break; - case 'close-payment-flow': - closePaymentFlow(); - break; - case 'close-promo-modal': - closePromoModal(); - break; - case 'close-referral-modal': - closeReferralModal(); - break; - case 'close-account-merge-modal': - closeAccountMergeModal(); - break; - case 'close-email-login-code-modal': - closeEmailLoginCodeModal(); - break; - case 'request-email-link-code': - requestEmailLinkCode(); - break; - case 'verify-email-link-code': - verifyEmailLinkCode(); - break; - case 'request-email-login-code': - requestEmailLoginCode(); - break; - case 'verify-email-login-code': - verifyEmailLoginCode(); - break; - case 'resend-email-login-code': - resendEmailLoginCode(); - break; - case 'set-auth-mode': - setAuthMode(target.dataset.mode || 'email'); - break; - case 'apply-promo-code': - applyPromoCode(); - break; - case 'select-plan': - selectPlan(target.dataset.months || ''); - break; - case 'create-payment': - createAndOpenPayment(target.dataset.method || ''); - break; - case 'copy-referral-link': - copyReferralLink(target.dataset.kind || 'webapp'); - break; - case 'copy-config-link': - copyConfigLink(); - break; - case 'check-payment': - checkPayment(); - break; - default: - return; - } - - event.preventDefault(); - } - - function openPromoModal() { - state.promoModalOpen = true; - clearPromoStatus(); - renderPromoModal(); - window.setTimeout(() => { - const input = document.getElementById('promo-code-input'); - if (input) input.focus(); - }, 80); - } - - function closePromoModal() { - state.promoModalOpen = false; - clearPromoStatus(); - renderPromoModal(); - } - - function renderPromoModal() { - const modal = document.getElementById('promo-modal'); - if (!modal) return; - - if (!state.promoModalOpen) { - modal.classList.remove('show'); - syncModalLock(); - window.setTimeout(() => { - if (!state.promoModalOpen) modal.classList.add('hidden'); - }, 180); - return; - } - - modal.classList.remove('hidden'); - syncModalLock(); - renderPromoStatus(); - applyI18n(modal); - window.requestAnimationFrame(() => modal.classList.add('show')); - } - - function bindPromoInput() { - const input = document.getElementById('promo-code-input'); - if (!input) return; - - input.addEventListener('input', () => { - if (input.getAttribute('aria-invalid') === 'true') { - input.removeAttribute('aria-invalid'); - } - if (state.promoStatusText) { - clearPromoStatus(); - } - }); - - input.addEventListener('keydown', event => { - if (event.key !== 'Enter') return; - event.preventDefault(); - applyPromoCode(); - }); - } - - function setPromoStatus(message, type = 'error') { - state.promoStatusType = type === 'success' ? 'success' : 'error'; - state.promoStatusText = normalizeStatusText(message || ''); - renderPromoStatus(); - } - - function clearPromoStatus() { - state.promoStatusType = ''; - state.promoStatusText = ''; - renderPromoStatus(); - } - - function renderPromoStatus() { - const status = document.getElementById('promo-status'); - if (!status) return; - - if (!state.promoStatusText) { - status.textContent = ''; - status.classList.add('hidden'); - status.classList.remove('error', 'success'); - return; - } - - status.textContent = state.promoStatusText; - status.classList.remove('hidden'); - status.classList.toggle('error', state.promoStatusType === 'error'); - status.classList.toggle('success', state.promoStatusType === 'success'); - } - - function normalizeStatusText(value) { - const text = String(value || ''); - const wrapper = document.createElement('div'); - wrapper.innerHTML = text; - return (wrapper.textContent || wrapper.innerText || '').trim(); - } - - function openReferralModal() { - state.referralModalOpen = true; - renderReferral(state.data && state.data.referral || {}); - } - - function closeReferralModal() { - state.referralModalOpen = false; - renderReferral(state.data && state.data.referral || {}); - } - - function closeToolModals() { - state.promoModalOpen = false; - state.referralModalOpen = false; - state.accountMergeModalOpen = false; - clearPromoStatus(); - renderPromoModal(); - renderReferral(state.data && state.data.referral || {}); - renderAccountMergeModal(); - } - - - - function openAccountMergeModal(notice = null) { - if (notice) { - state.accountMergeNotice = notice; - } - state.accountMergeModalOpen = true; - renderAccountMergeModal(); - } - - function closeAccountMergeModal() { - state.accountMergeModalOpen = false; - renderAccountMergeModal(); - } - - function renderAccountMergeModal() { - const modal = document.getElementById('account-merge-modal'); - const panel = document.getElementById('account-merge-panel'); - if (!modal || !panel) return; - - if (!state.accountMergeModalOpen) { - modal.classList.remove('show'); - syncModalLock(); - window.setTimeout(() => { - if (!state.accountMergeModalOpen) modal.classList.add('hidden'); - }, 180); - return; - } - - modal.classList.remove('hidden'); - syncModalLock(); - - const notice = state.accountMergeNotice || {}; - const primaryUserId = notice.primary_user_id != null ? String(notice.primary_user_id) : t('not_available'); - const removedUserId = notice.removed_user_id != null ? String(notice.removed_user_id) : t('not_available'); - const finalEndDateText = notice.final_end_date_text || t('not_available'); - - panel.innerHTML = ` -
-
-
${escapeHtml(t('account_merge_title'))}
-
${escapeHtml(t('account_merge_caption'))}
-
- -
- -
-
${escapeHtml(t('account_merge_body'))}
-
- -
-
-
${escapeHtml(t('account_merge_primary_account_label'))}
-
#${escapeHtml(primaryUserId)}
-
-
-
${escapeHtml(t('account_merge_removed_account_label'))}
-
#${escapeHtml(removedUserId)}
-
-
-
${escapeHtml(t('account_merge_end_date_label'))}
-
${escapeHtml(finalEndDateText)}
-
-
- `; - applyI18n(modal); - window.requestAnimationFrame(() => modal.classList.add('show')); - } - - function renderReferral(referral) { - const modal = document.getElementById('referral-modal'); - const panel = document.getElementById('referral-panel'); - if (!modal || !panel) return; - - if (!state.referralModalOpen) { - modal.classList.remove('show'); - syncModalLock(); - window.setTimeout(() => { - if (!state.referralModalOpen) modal.classList.add('hidden'); - }, 180); - return; - } - - modal.classList.remove('hidden'); - syncModalLock(); - if (!state.referralModalOpen) return; - - const bonusRows = (referral.bonus_details || []).map(item => ` -
- ${escapeHtml(item.title || '')} - ${escapeHtml(t('referral_bonus_pair', { - inviter: item.inviter_days || 0, - friend: item.friend_days || 0 - }))} -
- `).join(''); - - panel.innerHTML = ` -
-
-
${escapeHtml(t('referral_title'))}
-
${escapeHtml(t('referral_caption'))}
-
- -
- -
-
- - -
- -
-
- -
${escapeHtml(t('referral_bonus_explanation'))}
-
-
${bonusRows || '
' + escapeHtml(t('referral_no_bonuses')) + '
'}
-
-
- `; - applyI18n(modal); - window.requestAnimationFrame(() => modal.classList.add('show')); - } - - function renderReferralLinkRow(kind, label, link) { - return ` -
-
-
${escapeHtml(label)}
-
${escapeHtml(link || t('not_available'))}
-
- -
- `; - } - - async function applyPromoCode() { - if (state.promoApplying) return; - const input = document.getElementById('promo-code-input'); - const code = String(input && input.value || '').trim(); - if (!code) { - setPromoStatus(t('promo_empty'), 'error'); - if (input) input.setAttribute('aria-invalid', 'true'); - if (input) input.focus(); - return; - } - - state.promoApplying = true; - setButtonBusy('promo-apply-btn', true); - clearPromoStatus(); - if (input) input.removeAttribute('aria-invalid'); - try { - const data = await api('/promo/apply', { - method: 'POST', - body: JSON.stringify({code}) - }); - if (!data.ok) throw data; - if (input) input.value = ''; - setPromoStatus(t('promo_applied_ok'), 'success'); - await loadData(); - } catch (e) { - const errorCode = e && e.error; - const serverMessage = e && (e.message || e.detail); - if (input) input.setAttribute('aria-invalid', 'true'); - if (errorCode === 'empty_code') { - setPromoStatus(t('promo_empty'), 'error'); - } else if (serverMessage) { - setPromoStatus(serverMessage, 'error'); - } else if (errorCode === 'promo_apply_failed') { - setPromoStatus(t('promo_invalid'), 'error'); - } else { - setPromoStatus(t('payment_error'), 'error'); - } - } finally { - state.promoApplying = false; - setButtonBusy('promo-apply-btn', false); - } - } - - async function copyReferralLink(kind) { - const referral = state.data && state.data.referral; - const link = referral && (kind === 'bot' ? referral.bot_link : referral.webapp_link); - if (!link) { - showToast(t('no_link')); - return; - } - await copyText(link); - showToast(t('referral_copied')); - } - - async function copyReferralCode() { - const referral = state.data && state.data.referral; - const code = referral && referral.code; - if (!code) { - showToast(t('no_link')); - return; - } - await copyText(code); - showToast(t('code_copied')); - } - - async function requestEmailLinkCode() { - const input = document.getElementById('email-link-input'); - const email = normalizeEmail(input.value); - if (!email) { - showToast(t('email_code_send_failed')); - return; - } - state.emailLinkPending = true; - setButtonBusy('email-link-send-btn', true); - try { - const data = await api('/account/email/request', { - method: 'POST', - body: JSON.stringify({email}) - }); - if (!data.ok) throw data; - if (data.already_linked) { - showToast(t('email_linked')); - await loadData(); - return; - } - state.emailLinkEmail = email; - document.getElementById('email-link-code-row').classList.remove('hidden'); - showToast(t('email_code_sent')); - } catch (e) { - showToast(emailErrorMessage(e, 'email_code_send_failed')); - } finally { - state.emailLinkPending = false; - setButtonBusy('email-link-send-btn', false); - } - } - - async function verifyEmailLinkCode() { - const email = state.emailLinkEmail || normalizeEmail(document.getElementById('email-link-input').value); - const code = document.getElementById('email-link-code-input').value; - setButtonBusy('email-link-verify-btn', true); - let mergeNotice = null; - try { - const data = await api('/account/email/verify', { - method: 'POST', - body: JSON.stringify({email, code}) - }); - if (!data.ok) throw data; - if (data.token) setToken(data.token, data.csrf_token); - mergeNotice = data.account_merge && data.account_merge.merged ? data.account_merge : null; - state.accountMergeNotice = mergeNotice; - showToast(mergeNotice ? t('account_merge_toast') : t('email_linked')); - state.emailLinkEmail = ''; - try { - await loadData(); - if (mergeNotice) { - openAccountMergeModal(mergeNotice); - } - } catch (refreshError) { - console.warn('Email account linked, but data refresh failed', refreshError); - if (mergeNotice) { - openAccountMergeModal(mergeNotice); - } - } - } catch (e) { - showToast(emailErrorMessage(e, 'email_code_invalid')); - } finally { - setButtonBusy('email-link-verify-btn', false); - } - } - - function renderTelegramLinkWidget() { - const container = document.getElementById('telegram-link-widget'); - if (!container || state.telegramLinkRendered) return; - container.innerHTML = ''; - const botUsername = String(CFG.telegramLoginBotUsername || '').trim(); - if (!botUsername) { - setTelegramLinkStatus(t('telegram_bot_unavailable'), true); - return; - } - - window.onTelegramLinkAuth = async function(user) { - await linkTelegramAccount(user); - }; - - setTelegramLinkStatus(''); - appendTelegramLoginWidget( - container, - botUsername, - 'onTelegramLinkAuth', - () => setTelegramLinkStatus(t('telegram_auth_unavailable'), true) - ); - state.telegramLinkRendered = true; - } - - async function linkTelegramAccount(user) { - if (!user || state.telegramLinkInProgress) return; - state.telegramLinkInProgress = true; - setTelegramLinkStatus(t('telegram_auth_verifying')); - try { - const data = await api('/account/telegram/link', { - method: 'POST', - body: JSON.stringify({auth_data: user}) - }); - if (!data.ok) throw data; - await finishTelegramLink(data, user.id); - } catch (e) { - if (await refreshLinkedTelegramAfterError()) { - setTelegramLinkStatus(''); - try { - showToast(t('telegram_linked')); - } catch (toastError) { - console.warn('Telegram link success toast failed', toastError); - } - return; - } - setTelegramLinkStatus(emailErrorMessage(e, 'telegram_auth_failed'), true); - } finally { - state.telegramLinkInProgress = false; - } - } - - async function finishTelegramLink(data, fallbackTelegramId) { - if (data.token) setToken(data.token, data.csrf_token); - markTelegramLinked(data.telegram_id || fallbackTelegramId); - setTelegramLinkStatus(''); - state.telegramLinkRendered = false; - const mergeNotice = data.account_merge && data.account_merge.merged ? data.account_merge : null; - state.accountMergeNotice = mergeNotice; - try { - showToast(mergeNotice ? t('account_merge_toast') : t('telegram_linked')); - } catch (toastError) { - console.warn('Telegram link success toast failed', toastError); - } - try { - await loadData(); - if (mergeNotice) { - openAccountMergeModal(mergeNotice); - } - } catch (refreshError) { - console.warn('Telegram account linked, but data refresh failed', refreshError); - if (mergeNotice) { - openAccountMergeModal(mergeNotice); - } - } - } - - function markTelegramLinked(telegramId) { - if (!state.data || !state.data.user) return; - state.data.user.telegram_linked = true; - if (telegramId) { - state.data.user.telegram_id = Number(telegramId); - } - renderAccount(state.data.user); - } - - async function refreshLinkedTelegramAfterError() { - try { - await loadData(); - return Boolean(state.data && state.data.user && state.data.user.telegram_linked); - } catch (refreshError) { - console.warn('Telegram link status refresh failed', refreshError); - return false; - } - } - - function togglePaymentFlow() { - if (state.paymentFlowOpen) { - closePaymentFlow(); - return; - } - state.paymentFlowOpen = true; - state.creatingPayment = false; - state.payment = null; - state.selectedPlan = getDefaultPaymentPlan((state.data && state.data.plans) || []); - renderPaymentFlow(); - } - - function closePaymentFlow() { - state.paymentFlowOpen = false; - state.creatingPayment = false; - state.payment = null; - renderPaymentFlow(); - } - - function renderPaymentFlow() { - const modal = document.getElementById('payment-modal'); - if (!modal) return; - if (!state.paymentFlowOpen) { - modal.classList.remove('show'); - syncModalLock(); - window.setTimeout(() => { - if (!state.paymentFlowOpen) modal.classList.add('hidden'); - }, 180); - return; - } - - modal.classList.remove('hidden'); - syncModalLock(); - window.requestAnimationFrame(() => modal.classList.add('show')); - renderPlans((state.data && state.data.plans) || []); - renderPaymentSummary(); - renderMethods(); - applyI18n(modal); - } - - function syncModalLock() { - document.body.classList.toggle( - 'modal-open', - Boolean( - state.paymentFlowOpen - || state.emailLoginCodeModalOpen - || state.promoModalOpen - || state.referralModalOpen - || state.accountMergeModalOpen - ) - ); - } - - function getDefaultPaymentPlan(plans = []) { - if (!plans.length) return null; - return plans.find(plan => Number(plan.months) === 3) || plans[0] || null; - } - - function renderPlans(plans) { - const wrap = document.getElementById('plans'); - if (!wrap) return; - if (!plans.length) { - wrap.innerHTML = '
' + escapeHtml(t('no_plans')) + '
'; - return; - } - - if (!state.selectedPlan || !plans.some(plan => Number(plan.months) === Number(state.selectedPlan.months))) { - state.selectedPlan = getDefaultPaymentPlan(plans); - } - - wrap.innerHTML = plans.map(plan => { - const planMonths = Number(plan.months); - const isActive = state.selectedPlan && Number(state.selectedPlan.months) === planMonths; - return ` - - `; - }).join(''); - } - - function renderPaymentSummary() { - const price = document.getElementById('selected-plan-price'); - if (!price) return; - if (!state.selectedPlan) { - price.textContent = t('no_plans'); - return; - } - price.textContent = formatMoney(state.selectedPlan.price, state.selectedPlan.currency); - } - - function renderMethods() { - const methods = document.getElementById('payment-methods'); - if (!methods) return; - if (!state.selectedPlan) { - methods.innerHTML = '
' + escapeHtml(t('choose_plan_first')) + '
'; - return; - } - - const available = getAvailableMethods(); - if (!available.length) { - methods.innerHTML = '
' + escapeHtml(t('no_methods')) + '
'; - return; - } - - methods.innerHTML = available.map(method => { - let labelKey; - let variantClass; - if (method.id === 'platega_sbp') { - labelKey = 'pay_with_platega_sbp_button'; - variantClass = TW.paymentMethodCardPlategaSbp; - } else if (method.id === 'platega_crypto') { - labelKey = 'pay_with_platega_crypto_button'; - variantClass = TW.paymentMethodCardPlategaCrypto; - } else if (method.id === 'platega') { - labelKey = 'pay_with_platega_button'; - variantClass = TW.paymentMethodCardPlatega; - } else { - labelKey = 'pay_with_cryptopay_button'; - variantClass = TW.paymentMethodCardCryptopay; - } - return ` - - `; - }).join(''); - } - - function selectPlan(months) { - if (state.creatingPayment) return; - state.selectedPlan = ((state.data && state.data.plans) || []).find(plan => Number(plan.months) === Number(months)) || null; - state.payment = null; - renderPaymentFlow(); - } - - async function createAndOpenPayment(method) { - if (state.creatingPayment) return; - if (!state.selectedPlan) { - showToast(t('choose_plan_toast')); - return; - } - const available = getAvailableMethods(); - if (!available.some(item => item.id === method)) { - showToast(t('no_methods')); - return; - } - - state.creatingPayment = true; - state.payment = null; - renderPaymentFlow(); - try { - const data = await api('/payments', { - method: 'POST', - body: JSON.stringify({months: state.selectedPlan.months, method}) - }); - if (!data.ok || !data.payment_url) throw new Error(data.message || t('payment_error')); - state.payment = data; - const opened = openPaymentUrl(data.payment_url); - if (opened) { - closePaymentFlow(); - } - } catch (e) { - showToast(e.message || t('payment_error')); - } finally { - state.creatingPayment = false; - if (state.paymentFlowOpen) { - renderPaymentFlow(); - } - } - } - - function getAvailableMethods() { - if (!state.data || !state.selectedPlan) return []; - const methodsById = new Map( - (state.data.payment_methods || []).map(method => [String(method.id || '').toLowerCase(), method]) - ); - return ['platega_sbp', 'platega_crypto', 'platega', 'cryptopay'] - .map(methodId => methodsById.get(methodId)) - .filter(Boolean); - } - - function openPaymentUrl(url) { - const paymentUrl = String(url || (state.payment && state.payment.payment_url) || '').trim(); - if (!paymentUrl) return false; - if (tg && tg.openLink) { - tg.openLink(paymentUrl); - return true; - } - const opened = window.open(paymentUrl, '_blank', 'noopener'); - if (!opened) { - window.location.assign(paymentUrl); - } - return true; - } - - async function checkPayment() { - if (!state.payment || !state.payment.payment_id) return; - const data = await api('/payments/' + state.payment.payment_id); - if (data.paid) { - showToast(t('payment_confirmed')); - state.paymentFlowOpen = false; - await loadData(); - } else { - showToast(t('payment_pending')); - } - } - - function openConnectLink() { - const sub = state.data && state.data.subscription; - const url = sub && (sub.connect_url || sub.config_link); - if (!url) { - showToast(t('no_link')); - return; - } - if (tg && tg.openLink) tg.openLink(url); - else window.open(url, '_blank', 'noopener'); - } - - async function copyConfigLink() { - const sub = state.data && state.data.subscription; - const link = sub && sub.config_link; - if (!link) { - showToast(t('no_link')); - return; - } - await copyText(link); - showToast(t('link_copied')); - } - - async function copyText(value) { - try { - await navigator.clipboard.writeText(value); - } catch (e) { - const area = document.createElement('textarea'); - area.value = value; - document.body.appendChild(area); - area.select(); - document.execCommand('copy'); - area.remove(); - } - } - - async function api(path, options = {}) { - if (MOCK) { - return mockApi(path, options); - } - - const method = String(options.method || 'GET').toUpperCase(); - const headers = Object.assign({}, options.headers || {}); - if (state.token) { - headers.Authorization = 'Bearer ' + state.token; - } - const csrfToken = state.csrfToken || readCookie('rw_webapp_csrf') || ''; - if (csrfToken && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) { - headers['X-CSRF-Token'] = csrfToken; - } - if (options.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json'; - const response = await fetch(CFG.apiBase + path, Object.assign({}, options, {headers})); - if (response.status === 401) { - clearToken(); - await startExternalAuth(); - throw new Error('Unauthorized'); - } - return response.json(); - } - - async function publicApi(path, payload = {}) { - if (MOCK) { - return mockApi(path, { - method: 'POST', - body: JSON.stringify(payload) - }); - } - const response = await fetch(CFG.apiBase + path, { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(payload) - }); - return response.json(); - } - - async function mockApi(path, options = {}) { - await new Promise(resolve => window.setTimeout(resolve, 120)); - if (path === '/me') { - return JSON.parse(JSON.stringify(MOCK.data)); - } - if (path === '/auth/email/request' || path === '/account/email/request') { - return {ok: true}; - } - if (path === '/auth/email/verify') { - return {ok: true, token: 'local-preview', csrf_token: 'local-preview-csrf'}; - } - if (path === '/auth/email/magic') { - return {ok: true, token: 'local-preview', csrf_token: 'local-preview-csrf'}; - } - if (path === '/account/email/verify') { - MOCK.data.user.email = 'preview@example.com'; - MOCK.data.user.email_verified = true; - return {ok: true, token: 'local-preview', csrf_token: 'local-preview-csrf'}; - } - if (path === '/account/telegram/link') { - MOCK.data.user.telegram_linked = true; - MOCK.data.user.telegram_id = 100200300; - return {ok: true, token: 'local-preview', csrf_token: 'local-preview-csrf'}; - } - if (path === '/auth/logout') { - return {ok: true}; - } - if (path === '/promo/apply') { - return {ok: true, end_date_text: '20.05.2026 12:00'}; - } - if (path === '/payments' && String(options.method || '').toUpperCase() === 'POST') { - return { - ok: true, - action: 'open_link', - payment_url: 'https://example.com/payment-preview', - payment_id: 10001 - }; - } - if (path.startsWith('/payments/')) { - return { - ok: true, - payment_id: 10001, - status: 'pending', - paid: false - }; - } - return {ok: false, error: 'not_found'}; - } - - function setToken(token, csrfToken = '') { - clearManualLogoutFlag(); - state.token = token; - state.csrfToken = csrfToken || readCookie('rw_webapp_csrf') || state.csrfToken || ''; - } - - function clearToken() { - state.token = ''; - state.csrfToken = ''; - localStorage.removeItem('rw_webapp_token'); - } - - function clearReadableAuthCookie() { - document.cookie = 'rw_webapp_csrf=; Max-Age=0; path=/; SameSite=None; Secure'; - } - - function isManuallyLoggedOut() { - try { - return localStorage.getItem(MANUAL_LOGOUT_FLAG_KEY) === '1'; - } catch (e) { - return false; - } - } - - function markManualLogout() { - try { - localStorage.setItem(MANUAL_LOGOUT_FLAG_KEY, '1'); - } catch (e) { } - } - - function clearManualLogoutFlag() { - try { - localStorage.removeItem(MANUAL_LOGOUT_FLAG_KEY); - } catch (e) { } - } - - function readCookie(name) { - const prefix = name + '='; - const cookie = document.cookie.split('; ').find(part => part.startsWith(prefix)); - return cookie ? decodeURIComponent(cookie.slice(prefix.length)) : ''; - } - - function normalizeEmail(value) { - return String(value || '').trim().toLowerCase(); - } - - function emailErrorMessage(error, fallbackKey) { - const code = error && error.error; - if (code === 'rate_limited') { - return t('email_rate_limited', {seconds: error.retry_after || 60}); - } - if (code === 'invalid_email') return t('email_invalid'); - if (code === 'expired_code') return t('email_code_expired'); - if (code === 'invalid_code' || code === 'too_many_attempts') return t('email_code_invalid'); - if (code === 'email_auth_not_configured') return t('email_auth_disabled'); - if (code === 'account_merge_conflict') return t('account_merge_conflict'); - if (code === 'invalid_auth') return t('telegram_auth_failed'); - return t(fallbackKey); - } - - function setButtonBusy(id, busy) { - const button = document.getElementById(id); - if (button) button.disabled = Boolean(busy); - } - - async function logout() { - toggleUserMenu(false); - markManualLogout(); - clearReadableAuthCookie(); - clearToken(); - closePaymentFlow(); - closeEmailLoginCodeModal(); - startExternalAuth(); - try { - await publicApi('/auth/logout', {keepalive: true}); - } catch (e) { } - } - - function showLoader() { - document.getElementById('loader').classList.remove('hidden'); - document.getElementById('app').classList.add('hidden'); - document.getElementById('login').classList.remove('show'); - } - - function showApp() { - document.getElementById('loader').classList.add('hidden'); - document.getElementById('login').classList.remove('show'); - document.getElementById('login').classList.add('hidden'); - document.getElementById('app').classList.remove('hidden'); - } - - function showLogin() { - document.getElementById('loader').classList.add('hidden'); - document.getElementById('app').classList.add('hidden'); - document.getElementById('login').classList.remove('hidden'); - document.getElementById('login').classList.add('show'); - } - - function setAuthStatus(message, isError = false) { - const status = document.getElementById('auth-status'); - if (!status) return; - if (!message) { - status.textContent = ''; - status.classList.add('hidden'); - status.classList.remove('error'); - return; - } - - status.textContent = message; - status.classList.remove('hidden'); - status.classList.toggle('error', Boolean(isError)); - } - - function setEmailCodeStatus(message, isError = false) { - const status = document.getElementById('email-code-status'); - if (!status) return; - if (!message) { - status.textContent = ''; - status.classList.add('hidden'); - status.classList.remove('error'); - return; - } - - status.textContent = message; - status.classList.remove('hidden'); - status.classList.toggle('error', Boolean(isError)); - } - - function setTelegramLinkStatus(message, isError = false) { - const status = document.getElementById('telegram-link-status'); - if (!status) return; - if (!message) { - status.textContent = ''; - status.classList.add('hidden'); - status.classList.remove('error'); - return; - } - status.textContent = message; - status.classList.remove('hidden'); - status.classList.toggle('error', Boolean(isError)); - } - - function showToast(message) { - const toast = document.getElementById('toast'); - if (!toast) return; - - toast.textContent = message; - toast.classList.remove('hidden'); - - // Force reflow - void toast.offsetWidth; - - toast.classList.add('show'); - - // Telegram Haptic Feedback - if (tg && tg.HapticFeedback) { - try { - tg.HapticFeedback.notificationOccurred('success'); - } catch (e) { - console.warn('Haptic feedback failed', e); - } - } - - if (state.toastTimer) window.clearTimeout(state.toastTimer); - state.toastTimer = window.setTimeout(() => { - toast.classList.remove('show'); - window.setTimeout(() => { - if (!toast.classList.contains('show')) { - toast.classList.add('hidden'); - } - }, 250); - }, 2200); - } - - function setFavicon(href) { - let favicon = document.getElementById('app-favicon'); - if (!favicon) { - favicon = document.createElement('link'); - favicon.id = 'app-favicon'; - favicon.rel = 'icon'; - favicon.setAttribute('sizes', 'any'); - document.head.appendChild(favicon); - } - favicon.href = href || 'data:,'; - } - - function setBrandLogo(shell, logoUrl) { - if (!shell) return; - - const logo = shell.querySelector('[data-brand-logo]'); - const spinner = shell.querySelector('[data-brand-logo-spinner]'); - if (!logo) return; - - logo.onload = null; - logo.onerror = null; - logo.removeAttribute('data-brand-logo-url'); - - if (!logoUrl) { - logo.removeAttribute('src'); - logo.classList.add('hidden'); - if (spinner) spinner.classList.add('hidden'); - shell.classList.add('hidden'); - return; - } - - const expectedUrl = logoUrl; - shell.classList.remove('hidden'); - if (spinner) spinner.classList.remove('hidden'); - logo.classList.add('hidden'); - logo.dataset.brandLogoUrl = expectedUrl; - - logo.onload = () => { - if (logo.dataset.brandLogoUrl !== expectedUrl) return; - logo.classList.remove('hidden'); - if (spinner) spinner.classList.add('hidden'); - }; - - logo.onerror = () => { - if (logo.dataset.brandLogoUrl !== expectedUrl) return; - logo.removeAttribute('src'); - logo.classList.add('hidden'); - if (spinner) spinner.classList.add('hidden'); - shell.classList.add('hidden'); - }; - - logo.src = logoUrl; - if (logo.complete && logo.naturalWidth > 0) { - logo.classList.remove('hidden'); - if (spinner) spinner.classList.add('hidden'); - } - } - - function setBrand() { - const title = CFG.title || document.title || t('page_title'); - const logoUrl = CFG.logoUrl || ''; - document.title = title; - setFavicon(logoUrl); - document.querySelectorAll('[data-brand-title]').forEach(node => { - node.textContent = title; - }); - document.querySelectorAll('[data-brand-logo-shell]').forEach(shell => { - setBrandLogo(shell, logoUrl); - }); - } - - function applyI18n(root = document) { - document.documentElement.lang = getLanguage(); - root.querySelectorAll('[data-i18n]').forEach(node => { - node.textContent = t(node.dataset.i18n); - }); - root.querySelectorAll('[data-title-i18n]').forEach(node => { - const value = t(node.dataset.titleI18n); - node.setAttribute('title', value); - node.setAttribute('aria-label', value); - }); - root.querySelectorAll('[data-aria-i18n]').forEach(node => { - node.setAttribute('aria-label', t(node.dataset.ariaI18n)); - }); - root.querySelectorAll('[data-placeholder-i18n]').forEach(node => { - node.setAttribute('placeholder', t(node.dataset.placeholderI18n)); - }); - } - - function applyLegalLinks(root = document) { - const urls = { - privacyPolicyUrl: CFG.privacyPolicyUrl || '', - userAgreementUrl: CFG.userAgreementUrl || '' - }; - - root.querySelectorAll('[data-legal-key]').forEach(node => { - const key = node.dataset.legalKey; - const url = urls[key] || ''; - node.href = url || '#'; - node.classList.toggle('hidden', !url); - node.setAttribute('aria-hidden', url ? 'false' : 'true'); - }); - - root.querySelectorAll('[data-legal-links]').forEach(container => { - const visible = Array.from(container.querySelectorAll('[data-legal-key]')) - .some(node => !node.classList.contains('hidden')); - container.classList.toggle('hidden', !visible); - }); - } - - function getLanguage() { - const candidates = []; - if (state.langOverride) candidates.push(state.langOverride); - if (state.data && state.data.user && state.data.user.language_code) { - candidates.push(state.data.user.language_code); - } - const tgUser = tg && tg.initDataUnsafe && tg.initDataUnsafe.user; - if (tgUser && tgUser.language_code) candidates.push(tgUser.language_code); - if (typeof navigator !== 'undefined') { - if (Array.isArray(navigator.languages)) { - navigator.languages.forEach(l => l && candidates.push(l)); - } - if (navigator.language) candidates.push(navigator.language); - } - if (CFG.language) candidates.push(CFG.language); - if (document.documentElement.lang) candidates.push(document.documentElement.lang); - for (const raw of candidates) { - const short = String(raw).toLowerCase().split('-')[0]; - if (I18N && I18N[short]) return short; - } - return 'ru'; - } - - function setLanguage(lang) { - const short = String(lang || '').toLowerCase().split('-')[0]; - if (I18N && Object.keys(I18N).length && !I18N[short]) return; - state.langOverride = short; - try { localStorage.setItem('rw_webapp_lang', short); } catch (e) { } - applyI18n(); - if (state.data) { - try { render(); } catch (e) { } - } - renderLangMenu(); - toggleLangMenu(false); - } - - function t(key, params = {}) { - const table = (I18N && I18N[getLanguage()]) || (I18N && I18N.ru) || {}; - const fallback = (I18N && I18N.ru && I18N.ru[key]) || key; - const template = table[key] || fallback; - return template.replace(/\{(\w+)\}/g, (_, name) => ( - Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : '' - )); - } - - function formatMoney(value, currency) { - const numeric = Number(value || 0); - const formatted = Number.isInteger(numeric) ? String(numeric) : numeric.toFixed(2); - return formatted + ' ' + (currency || CFG.currency || 'RUB'); - } - - function escapeHtml(value) { - return String(value == null ? '' : value) - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", '''); - } - - function escapeAttr(value) { - return escapeHtml(value).replaceAll('`', '`'); - } +(function() { + //#region node_modules/svelte/src/internal/shared/utils.js + var is_array = Array.isArray; + var index_of = Array.prototype.indexOf; + var includes = Array.prototype.includes; + var array_from = Array.from; + var define_property = Object.defineProperty; + var get_descriptor = Object.getOwnPropertyDescriptor; + var get_descriptors = Object.getOwnPropertyDescriptors; + var object_prototype = Object.prototype; + var array_prototype = Array.prototype; + var get_prototype_of = Object.getPrototypeOf; + var is_extensible = Object.isExtensible; + /** + * @param {any} thing + * @returns {thing is Function} + */ + function is_function(thing) { + return typeof thing === "function"; + } + var noop = () => {}; + /** @param {Function} fn */ + function run(fn) { + return fn(); + } + /** @param {Array<() => void>} arr */ + function run_all(arr) { + for (var i = 0; i < arr.length; i++) arr[i](); + } + /** + * TODO replace with Promise.withResolvers once supported widely enough + * @template [T=void] + */ + function deferred() { + /** @type {(value: T) => void} */ + var resolve; + /** @type {(reason: any) => void} */ + var reject; + return { + promise: new Promise((res, rej) => { + resolve = res; + reject = rej; + }), + resolve, + reject + }; + } + /** + * When encountering a situation like `let [a, b, c] = $derived(blah())`, + * we need to stash an intermediate value that `a`, `b`, and `c` derive + * from, in case it's an iterable + * @template T + * @param {ArrayLike | Iterable} value + * @param {number} [n] + * @returns {Array} + */ + function to_array(value, n) { + if (Array.isArray(value)) return value; + if (n === void 0 || !(Symbol.iterator in value)) return Array.from(value); + /** @type {T[]} */ + const array = []; + for (const element of value) { + array.push(element); + if (array.length === n) break; + } + return array; + } + //#endregion + //#region node_modules/svelte/src/internal/client/constants.js + /** + * An effect that does not destroy its child effects when it reruns. + * Runs as part of render effects, i.e. not eagerly as part of tree traversal or effect flushing. + */ + var MANAGED_EFFECT = 1 << 24; + var CLEAN = 1024; + var DIRTY = 2048; + var MAYBE_DIRTY = 4096; + var INERT = 8192; + var DESTROYED = 16384; + /** Set once a reaction has run for the first time */ + var REACTION_RAN = 32768; + /** Effect is in the process of getting destroyed. Can be observed in child teardown functions */ + var DESTROYING = 1 << 25; + /** + * 'Transparent' effects do not create a transition boundary. + * This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned + */ + var EFFECT_TRANSPARENT = 65536; + var HEAD_EFFECT = 1 << 18; + var EFFECT_PRESERVED = 1 << 19; + var USER_EFFECT = 1 << 20; + var EFFECT_OFFSCREEN = 1 << 25; + /** + * Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase. + * Will be lifted during execution of the derived and during checking its dirty state (both are necessary + * because a derived might be checked but not executed). This is a pure performance optimization flag and + * should not be used for any other purpose! + */ + var WAS_MARKED = 65536; + var REACTION_IS_UPDATING = 1 << 21; + var ASYNC = 1 << 22; + var ERROR_VALUE = 1 << 23; + var STATE_SYMBOL = Symbol("$state"); + var LEGACY_PROPS = Symbol("legacy props"); + var LOADING_ATTR_SYMBOL = Symbol(""); + /** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */ + var STALE_REACTION = new class StaleReactionError extends Error { + name = "StaleReactionError"; + message = "The reaction that called `getAbortSignal()` was re-run or destroyed"; + }(); + var IS_XHTML = !!globalThis.document?.contentType && /* @__PURE__ */ globalThis.document.contentType.includes("xml"); + /** + * `%name%(...)` can only be used during component initialisation + * @param {string} name + * @returns {never} + */ + function lifecycle_outside_component(name) { + throw new Error(`https://svelte.dev/e/lifecycle_outside_component`); + } + //#endregion + //#region node_modules/svelte/src/internal/client/errors.js + /** + * Cannot create a `$derived(...)` with an `await` expression outside of an effect tree + * @returns {never} + */ + function async_derived_orphan() { + throw new Error(`https://svelte.dev/e/async_derived_orphan`); + } + /** + * Keyed each block has duplicate key `%value%` at indexes %a% and %b% + * @param {string} a + * @param {string} b + * @param {string | undefined | null} [value] + * @returns {never} + */ + function each_key_duplicate(a, b, value) { + throw new Error(`https://svelte.dev/e/each_key_duplicate`); + } + /** + * `%rune%` cannot be used inside an effect cleanup function + * @param {string} rune + * @returns {never} + */ + function effect_in_teardown(rune) { + throw new Error(`https://svelte.dev/e/effect_in_teardown`); + } + /** + * Effect cannot be created inside a `$derived` value that was not itself created inside an effect + * @returns {never} + */ + function effect_in_unowned_derived() { + throw new Error(`https://svelte.dev/e/effect_in_unowned_derived`); + } + /** + * `%rune%` can only be used inside an effect (e.g. during component initialisation) + * @param {string} rune + * @returns {never} + */ + function effect_orphan(rune) { + throw new Error(`https://svelte.dev/e/effect_orphan`); + } + /** + * Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state + * @returns {never} + */ + function effect_update_depth_exceeded() { + throw new Error(`https://svelte.dev/e/effect_update_depth_exceeded`); + } + /** + * Cannot do `bind:%key%={undefined}` when `%key%` has a fallback value + * @param {string} key + * @returns {never} + */ + function props_invalid_value(key) { + throw new Error(`https://svelte.dev/e/props_invalid_value`); + } + /** + * Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. + * @returns {never} + */ + function state_descriptors_fixed() { + throw new Error(`https://svelte.dev/e/state_descriptors_fixed`); + } + /** + * Cannot set prototype of `$state` object + * @returns {never} + */ + function state_prototype_fixed() { + throw new Error(`https://svelte.dev/e/state_prototype_fixed`); + } + /** + * Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state` + * @returns {never} + */ + function state_unsafe_mutation() { + throw new Error(`https://svelte.dev/e/state_unsafe_mutation`); + } + /** + * A `` `reset` function cannot be called while an error is still being handled + * @returns {never} + */ + function svelte_boundary_reset_onerror() { + throw new Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`); + } + //#endregion + //#region node_modules/svelte/src/constants.js + var HYDRATION_ERROR = {}; + var UNINITIALIZED = Symbol(); + var NAMESPACE_HTML = "http://www.w3.org/1999/xhtml"; + var NAMESPACE_SVG = "http://www.w3.org/2000/svg"; + /** + * Reading a derived belonging to a now-destroyed effect may result in stale values + */ + function derived_inert() { + console.warn(`https://svelte.dev/e/derived_inert`); + } + /** + * Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location% + * @param {string | undefined | null} [location] + */ + function hydration_mismatch(location) { + console.warn(`https://svelte.dev/e/hydration_mismatch`); + } + /** + * The `value` property of a ``); + function Input($$anchor, $$props) { + push($$props, false); + let value = prop($$props, "value", 12, ""); + let type = prop($$props, "type", 8, "text"); + let placeholder = prop($$props, "placeholder", 8, ""); + let inputmode = prop($$props, "inputmode", 8, void 0); + let maxlength = prop($$props, "maxlength", 8, void 0); + let autocomplete = prop($$props, "autocomplete", 8, void 0); + let disabled = prop($$props, "disabled", 8, false); + let className = prop($$props, "class", 8, ""); + init(); + var input = root$5(); + remove_input_defaults(input); + template_effect(($0) => { + set_class(input, 1, $0); + set_attribute(input, "type", type()); + set_attribute(input, "placeholder", placeholder()); + set_attribute(input, "inputmode", inputmode()); + set_attribute(input, "maxlength", maxlength()); + set_attribute(input, "autocomplete", autocomplete()); + input.disabled = disabled(); + }, [() => clsx((deep_read_state(cn), deep_read_state(className()), untrack(() => cn("input", className()))))]); + bind_value(input, value); + append($$anchor, input); + pop(); + } + //#endregion + //#region bot/app/web/frontend/src/preview/BackTitle.svelte + var root$4 = /* @__PURE__ */ from_html(`

`); + function BackTitle($$anchor, $$props) { + let title = prop($$props, "title", 8, ""); + let subtitle = prop($$props, "subtitle", 8, ""); + var header = root$4(); + var button = child(header); + Arrow_left(child(button), { size: 18 }); + reset(button); + var div = sibling(button, 2); + var h1 = child(div); + var text = child(h1, true); + reset(h1); + var p = sibling(h1, 2); + var text_1 = child(p, true); + reset(p); + reset(div); + next(2); + reset(header); + template_effect(() => { + set_text(text, title()); + set_text(text_1, subtitle()); + }); + append($$anchor, header); + } + //#endregion + //#region bot/app/web/frontend/src/preview/PhoneFrame.svelte + var root$3 = /* @__PURE__ */ from_html(`

`); + function PhoneFrame($$anchor, $$props) { + let number = prop($$props, "number", 8, ""); + let label = prop($$props, "label", 8, ""); + let wide = prop($$props, "wide", 8, false); + var section = root$3(); + let classes; + var h2 = child(section); + var span = child(h2); + var text = child(span); + reset(span); + var text_1 = sibling(span); + reset(h2); + var div = sibling(h2, 2); + slot(child(div), $$props, "default", {}, null); + reset(div); + reset(section); + template_effect(() => { + classes = set_class(section, 1, "preview-phone-wrap", null, classes, { wide: wide() }); + set_text(text, `${number() ?? ""}.`); + set_text(text_1, ` ${label() ?? ""}`); + }); + append($$anchor, section); + } + //#endregion + //#region bot/app/web/frontend/src/preview/PreviewMethods.svelte + var root_1$1 = /* @__PURE__ */ from_html(`
`); + var root$2 = /* @__PURE__ */ from_html(`
`); + function PreviewMethods($$anchor, $$props) { + let methods = prop($$props, "methods", 24, () => []); + const icons = [ + Credit_card, + Send, + Wallet_cards, + Wallet_cards + ]; + function note(index) { + if (index === 0) return "Visa, Mastercard"; + if (index === 1) return "Быстро и удобно"; + if (index === 2) return "USDT, BTC, ETH"; + return "ЮMoney, СБП и др."; + } + var div = root$2(); + each(div, 5, methods, index, ($$anchor, method, index) => { + var div_1 = root_1$1(); + set_class(div_1, 1, "method-card", null, {}, { active: index === 1 }); + var node = child(div_1); + component(node, () => icons[index] || Wallet_cards, ($$anchor, $$component) => { + $$component($$anchor, { size: 18 }); + }); + var span = sibling(node, 2); + var strong = child(span); + var text = child(strong, true); + reset(strong); + var small = sibling(strong, 2); + var text_1 = child(small, true); + reset(small); + reset(span); + reset(div_1); + template_effect(($0) => { + set_text(text, (get(method), untrack(() => get(method).name))); + set_text(text_1, $0); + }, [() => untrack(() => note(index))]); + append($$anchor, div_1); + }); + reset(div); + append($$anchor, div); + } + //#endregion + //#region bot/app/web/frontend/src/preview/PreviewNav.svelte + var root$1 = /* @__PURE__ */ from_html(``); + function PreviewNav($$anchor, $$props) { + let active = prop($$props, "active", 8, "home"); + var nav = root$1(); + var button = child(nav); + let classes; + House(child(button), { size: 20 }); + next(); + reset(button); + var button_1 = sibling(button, 2); + let classes_1; + Gift(child(button_1), { size: 20 }); + next(); + reset(button_1); + var button_2 = sibling(button_1, 2); + let classes_2; + Settings(child(button_2), { size: 20 }); + next(); + reset(button_2); + reset(nav); + template_effect(() => { + classes = set_class(button, 1, "", null, classes, { active: active() === "home" }); + classes_1 = set_class(button_1, 1, "", null, classes_1, { active: active() === "invite" }); + classes_2 = set_class(button_2, 1, "", null, classes_2, { active: active() === "settings" }); + }); + append($$anchor, nav); + } + //#endregion + //#region bot/app/web/frontend/src/PreviewBoard.svelte + var root_2 = /* @__PURE__ */ from_html(`

Подписка активна

`); + var root_3$1 = /* @__PURE__ */ from_html(`
Использовано трафика
18%
`, 1); + var root_4$1 = /* @__PURE__ */ from_html(`Установить и настроить`, 1); + var root_5$1 = /* @__PURE__ */ from_html(`Продлить`, 1); + var root_6$1 = /* @__PURE__ */ from_html(`Сменить тариф`, 1); + var root_1 = /* @__PURE__ */ from_html(`
`); + var root_8$1 = /* @__PURE__ */ from_html(`
`); + var root_11 = /* @__PURE__ */ from_html(`Далее `, 1); + var root_7$1 = /* @__PURE__ */ from_html(`
`, 1); + var root_13$1 = /* @__PURE__ */ from_html(`
`); + var root_15$1 = /* @__PURE__ */ from_html(`Итого
К оплате
790 ₽`, 1); + var root_16$1 = /* @__PURE__ */ from_html(`Оплатить 790 ₽ `, 1); + var root_12$1 = /* @__PURE__ */ from_html(`
`, 1); + var root_18$1 = /* @__PURE__ */ from_html(`
`); + var root_20 = /* @__PURE__ */ from_html(`Итого
К оплате
990 ₽`, 1); + var root_21$1 = /* @__PURE__ */ from_html(`Оплатить 990 ₽ `, 1); + var root_17$1 = /* @__PURE__ */ from_html(`
`, 1); + var root_23$1 = /* @__PURE__ */ from_html(`Далее `, 1); + var root_22$1 = /* @__PURE__ */ from_html(`
ПодпискаБезлимитный трафикДоплата 190 ₽
ТрафикПакеты гигабайтДоплата не требуется
ПремиумМаксимальная скоростьДоплата 390 ₽
Сменить тариф без доплаты?

Остаток 12 дней будет пересчитан по новому тарифу.

`, 1); + var root_28$1 = /* @__PURE__ */ from_html(`Копировать `, 1); + var root_27$1 = /* @__PURE__ */ from_html(`
Ваша реферальная ссылка
https://minishop.app/ref/ABCD1234
`, 1); + var root_29$1 = /* @__PURE__ */ from_html(`
Ваш бонус+7 дней за каждого друга

Друг получит +3 дня к подписке.

`, 1); + var root_30$1 = /* @__PURE__ */ from_html(`Активировать промокод`, 1); + var root_26$1 = /* @__PURE__ */ from_html(`
`, 1); + var root_33$1 = /* @__PURE__ */ from_html(`Аватар пользователя`); + var root_32$1 = /* @__PURE__ */ from_html(`
`, 1); + var root_35$1 = /* @__PURE__ */ from_html(`
`); + var root_31$1 = /* @__PURE__ */ from_html(`
`, 1); + var root_38$1 = /* @__PURE__ */ from_html(`Получить код`, 1); + var root_39 = /* @__PURE__ */ from_html(`Войти через Telegram`, 1); + var root_37 = /* @__PURE__ */ from_html(`
Вход по email
или
`, 1); + var root_36$1 = /* @__PURE__ */ from_html(`
Нет аккаунта? Создать
`, 1); + var root_41$1 = /* @__PURE__ */ from_html(` `); + var root_40 = /* @__PURE__ */ from_html(`
`, 1); + var root = /* @__PURE__ */ from_html(`
`); + function PreviewBoard($$anchor, $$props) { + push($$props, false); + let config = prop($$props, "config", 24, () => ({})); + let mockData = prop($$props, "mockData", 24, () => ({})); + const title = config().title || "/minishop"; + const logoEmoji = config().logoEmoji || "🫥"; + const plans = mockData().plans || []; + const sub = mockData().subscription || {}; + const methods = mockData().payment_methods || []; + const user = mockData().user || {}; + const tariffs = [ + [ + "subscription", + "Подписка", + "Безлимитный трафик", + "Идеально для постоянного использования", + Zap + ], + [ + "traffic", + "Трафик", + "Пакеты гигабайт", + "Платите только за нужный объем", + Database + ], + [ + "premium", + "Премиум", + "Максимальная скорость", + "Приоритетные серверы и поддержка", + Crown + ] + ]; + const traffic = [ + [20, 290], + [50, 590], + [100, 990], + [300, 2190] + ]; + const settingsRows = [ + [ + Earth, + "Язык интерфейса", + "Русский" + ], + [ + Send, + "Привязка Telegram", + user.telegram_linked ? `@${user.username || "username"}` : "Не привязан" + ], + [ + Mail, + "Привязка почты", + user.email || "Не привязана" + ], + [ + User_round, + "Выйти", + "Завершить сессию" + ] + ]; + const previewTelegramName = user.first_name || (user.username ? `@${user.username}` : "Telegram не привязан"); + const previewEmail = user.email || "Почта не привязана"; + const previewTelegramId = user.telegram_id ? `TG ID ${user.telegram_id}` : "TG ID не привязан"; + const previewAvatar = user.telegram_photo_url || ""; + function money(value) { + return `${value} ₽`; + } + init(); + var div = root(); + var node = child(div); + PhoneFrame(node, { + number: "1", + label: "Главный экран", + children: ($$anchor, $$slotProps) => { + var main = root_1(); + var div_1 = child(main); + var div_2 = child(div_1); + var span = child(div_2); + var text = child(span, true); + reset(span); + reset(div_2); + var h1 = sibling(div_2, 2); + var text_1 = child(h1, true); + reset(h1); + reset(div_1); + var div_3 = sibling(div_1, 2); + var node_1 = child(div_3); + Card(node_1, { + class: "status-card", + children: ($$anchor, $$slotProps) => { + var div_4 = root_2(); + var node_2 = child(div_4); + Circle_check(node_2, { size: 23 }); + var div_5 = sibling(node_2, 2); + var p = sibling(child(div_5)); + var text_2 = child(p); + reset(p); + reset(div_5); + reset(div_4); + template_effect(() => set_text(text_2, `до ${untrack(() => sub.end_date_text) ?? ""}`)); + append($$anchor, div_4); + }, + $$slots: { default: true } + }); + var node_3 = sibling(node_1, 2); + Card(node_3, { + children: ($$anchor, $$slotProps) => { + var fragment = root_3$1(); + var div_6 = first_child(fragment); + var strong = sibling(child(div_6)); + var text_3 = child(strong); + reset(strong); + reset(div_6); + next(4); + template_effect(() => set_text(text_3, `${untrack(() => sub.traffic_used) ?? ""} из ${untrack(() => sub.traffic_limit) ?? ""}`)); + append($$anchor, fragment); + }, + $$slots: { default: true } + }); + var div_7 = sibling(node_3, 2); + var node_4 = child(div_7); + Button(node_4, { + class: "wide", + children: ($$anchor, $$slotProps) => { + var fragment_1 = root_4$1(); + Download(first_child(fragment_1), { size: 17 }); + next(); + append($$anchor, fragment_1); + }, + $$slots: { default: true } + }); + var node_6 = sibling(node_4, 2); + Button(node_6, { + variant: "secondary", + class: "wide", + children: ($$anchor, $$slotProps) => { + var fragment_2 = root_5$1(); + Refresh_cw(first_child(fragment_2), { size: 17 }); + next(); + append($$anchor, fragment_2); + }, + $$slots: { default: true } + }); + Button(sibling(node_6, 2), { + variant: "secondary", + class: "wide", + children: ($$anchor, $$slotProps) => { + var fragment_3 = root_6$1(); + Repeat_2(first_child(fragment_3), { size: 17 }); + next(); + append($$anchor, fragment_3); + }, + $$slots: { default: true } + }); + reset(div_7); + reset(div_3); + PreviewNav(sibling(div_3, 2), { active: "home" }); + reset(main); + template_effect(() => { + set_text(text, logoEmoji); + set_text(text_1, title); + }); + append($$anchor, main); + }, + $$slots: { default: true } + }); + var node_11 = sibling(node, 2); + PhoneFrame(node_11, { + number: "2", + label: "Выбор тарифа", + children: ($$anchor, $$slotProps) => { + var fragment_4 = root_7$1(); + var div_8 = first_child(fragment_4); + var div_9 = child(div_8); + var div_10 = child(div_9); + var span_1 = child(div_10); + var text_4 = child(span_1, true); + reset(span_1); + reset(div_10); + var strong_1 = sibling(div_10); + var text_5 = child(strong_1, true); + reset(strong_1); + reset(div_9); + reset(div_8); + var div_11 = sibling(div_8, 2); + each(div_11, 5, () => tariffs, index, ($$anchor, tariff, index) => { + var div_12 = root_8$1(); + set_class(div_12, 1, "select-card", null, {}, { active: index === 0 }); + var span_2 = child(div_12); + component(child(span_2), () => get(tariff)[4], ($$anchor, $$component) => { + $$component($$anchor, { size: 24 }); + }); + reset(span_2); + var span_3 = sibling(span_2, 2); + var strong_2 = child(span_3); + var text_6 = child(strong_2, true); + reset(strong_2); + var small = sibling(strong_2); + var text_7 = child(small, true); + reset(small); + var em = sibling(small); + var text_8 = child(em, true); + reset(em); + reset(span_3); + var node_13 = sibling(span_3, 2); + var consequent = ($$anchor) => { + Circle_check($$anchor, { size: 21 }); + }; + var alternate = ($$anchor) => { + Circle($$anchor, { size: 21 }); + }; + if_block(node_13, ($$render) => { + if (index === 0) $$render(consequent); + else $$render(alternate, -1); + }); + reset(div_12); + template_effect(() => { + set_text(text_6, (get(tariff), untrack(() => get(tariff)[1]))); + set_text(text_7, (get(tariff), untrack(() => get(tariff)[2]))); + set_text(text_8, (get(tariff), untrack(() => get(tariff)[3]))); + }); + append($$anchor, div_12); + }); + reset(div_11); + Button(sibling(div_11, 2), { + class: "wide bottom-action", + children: ($$anchor, $$slotProps) => { + next(); + var fragment_7 = root_11(); + Arrow_right(sibling(first_child(fragment_7)), { size: 17 }); + append($$anchor, fragment_7); + }, + $$slots: { default: true } + }); + template_effect(() => { + set_text(text_4, logoEmoji); + set_text(text_5, title); + }); + append($$anchor, fragment_4); + }, + $$slots: { default: true } + }); + var node_16 = sibling(node_11, 2); + PhoneFrame(node_16, { + number: "3", + label: "Оплата тарифа — подписка", + wide: true, + children: ($$anchor, $$slotProps) => { + var fragment_8 = root_12$1(); + var node_17 = first_child(fragment_8); + BackTitle(node_17, { + title: "Подписка", + subtitle: "Выберите срок подписки" + }); + var div_13 = sibling(node_17, 2); + each(div_13, 5, () => plans, index, ($$anchor, plan, index) => { + var div_14 = root_13$1(); + set_class(div_14, 1, "period-card", null, {}, { active: index === 1 }); + var strong_3 = child(div_14); + var text_9 = child(strong_3, true); + reset(strong_3); + var span_4 = sibling(strong_3); + var text_10 = child(span_4, true); + reset(span_4); + var small_1 = sibling(span_4); + var text_11 = child(small_1); + reset(small_1); + var node_18 = sibling(small_1, 2); + var consequent_1 = ($$anchor) => { + Circle_check($$anchor, { size: 18 }); + }; + if_block(node_18, ($$render) => { + if (index === 1) $$render(consequent_1); + }); + reset(div_14); + template_effect(($0, $1) => { + set_text(text_9, (get(plan), untrack(() => get(plan).title))); + set_text(text_10, $0); + set_text(text_11, `${$1 ?? ""}/мес`); + }, [() => (get(plan), untrack(() => money(get(plan).price))), () => (get(plan), untrack(() => money(Math.round(get(plan).price / get(plan).months))))]); + append($$anchor, div_14); + }); + reset(div_13); + var node_19 = sibling(div_13, 2); + Card(node_19, { + class: "total-card", + children: ($$anchor, $$slotProps) => { + var fragment_10 = root_15$1(); + next(); + append($$anchor, fragment_10); + }, + $$slots: { default: true } + }); + var node_20 = sibling(node_19, 2); + PreviewMethods(node_20, { get methods() { + return methods; + } }); + Button(sibling(node_20, 2), { + class: "wide bottom-action", + children: ($$anchor, $$slotProps) => { + next(); + var fragment_11 = root_16$1(); + Lock_keyhole(sibling(first_child(fragment_11)), { size: 16 }); + append($$anchor, fragment_11); + }, + $$slots: { default: true } + }); + append($$anchor, fragment_8); + }, + $$slots: { default: true } + }); + var node_23 = sibling(node_16, 2); + PhoneFrame(node_23, { + number: "4", + label: "Оплата тарифа — трафик", + wide: true, + children: ($$anchor, $$slotProps) => { + var fragment_12 = root_17$1(); + var node_24 = first_child(fragment_12); + BackTitle(node_24, { + title: "Трафик", + subtitle: "Выберите пакет трафика" + }); + var div_15 = sibling(node_24, 2); + each(div_15, 5, () => traffic, index, ($$anchor, pack, index) => { + var div_16 = root_18$1(); + set_class(div_16, 1, "period-card", null, {}, { active: index === 2 }); + var strong_4 = child(div_16); + var text_12 = child(strong_4); + reset(strong_4); + var span_5 = sibling(strong_4); + var text_13 = child(span_5, true); + reset(span_5); + var small_2 = sibling(span_5); + var text_14 = child(small_2); + reset(small_2); + var node_25 = sibling(small_2, 2); + var consequent_2 = ($$anchor) => { + Circle_check($$anchor, { size: 18 }); + }; + if_block(node_25, ($$render) => { + if (index === 2) $$render(consequent_2); + }); + reset(div_16); + template_effect(($0, $1) => { + set_text(text_12, `${(get(pack), untrack(() => get(pack)[0])) ?? ""} ГБ`); + set_text(text_13, $0); + set_text(text_14, `${$1 ?? ""}/ГБ`); + }, [() => (get(pack), untrack(() => money(get(pack)[1]))), () => (get(pack), untrack(() => money(Math.round(get(pack)[1] / get(pack)[0]))))]); + append($$anchor, div_16); + }); + reset(div_15); + var node_26 = sibling(div_15, 2); + Card(node_26, { + class: "total-card", + children: ($$anchor, $$slotProps) => { + var fragment_14 = root_20(); + next(); + append($$anchor, fragment_14); + }, + $$slots: { default: true } + }); + var node_27 = sibling(node_26, 2); + PreviewMethods(node_27, { get methods() { + return methods; + } }); + Button(sibling(node_27, 2), { + class: "wide bottom-action", + children: ($$anchor, $$slotProps) => { + next(); + var fragment_15 = root_21$1(); + Lock_keyhole(sibling(first_child(fragment_15)), { size: 16 }); + append($$anchor, fragment_15); + }, + $$slots: { default: true } + }); + append($$anchor, fragment_12); + }, + $$slots: { default: true } + }); + var node_30 = sibling(node_23, 2); + PhoneFrame(node_30, { + number: "5", + label: "Смена тарифа", + children: ($$anchor, $$slotProps) => { + var fragment_16 = root_22$1(); + var node_31 = first_child(fragment_16); + BackTitle(node_31, { + title: "Смена тарифа", + subtitle: "Остаток 12 дней будет пересчитан" + }); + var div_17 = sibling(node_31, 2); + var div_18 = child(div_17); + Circle(sibling(child(div_18), 2), { size: 20 }); + reset(div_18); + var div_19 = sibling(div_18, 2); + Circle_check(sibling(child(div_19), 2), { size: 20 }); + reset(div_19); + var div_20 = sibling(div_19, 2); + Circle(sibling(child(div_20), 2), { size: 20 }); + reset(div_20); + reset(div_17); + var node_35 = sibling(div_17, 2); + Button(node_35, { + class: "wide bottom-action", + children: ($$anchor, $$slotProps) => { + next(); + var fragment_17 = root_23$1(); + Arrow_right(sibling(first_child(fragment_17)), { size: 17 }); + append($$anchor, fragment_17); + }, + $$slots: { default: true } + }); + var div_21 = sibling(node_35, 2); + var node_37 = child(div_21); + Repeat_2(node_37, { size: 30 }); + var node_38 = sibling(node_37, 6); + Button(node_38, { + children: ($$anchor, $$slotProps) => { + next(); + append($$anchor, text("Да, сменить")); + }, + $$slots: { default: true } + }); + Button(sibling(node_38, 2), { + variant: "secondary", + children: ($$anchor, $$slotProps) => { + next(); + append($$anchor, text("Отмена")); + }, + $$slots: { default: true } + }); + reset(div_21); + append($$anchor, fragment_16); + }, + $$slots: { default: true } + }); + var node_40 = sibling(node_30, 2); + PhoneFrame(node_40, { + number: "6", + label: "Пригласить друга", + children: ($$anchor, $$slotProps) => { + var fragment_18 = root_26$1(); + var div_22 = first_child(fragment_18); + var div_23 = child(div_22); + var div_24 = child(div_23); + var span_6 = child(div_24); + var text_17 = child(span_6, true); + reset(span_6); + reset(div_24); + var strong_5 = sibling(div_24); + var text_18 = child(strong_5, true); + reset(strong_5); + reset(div_23); + reset(div_22); + var node_41 = sibling(div_22, 2); + Card(node_41, { + children: ($$anchor, $$slotProps) => { + var fragment_19 = root_27$1(); + var div_25 = sibling(first_child(fragment_19), 2); + Button(sibling(child(div_25)), { + children: ($$anchor, $$slotProps) => { + next(); + var fragment_20 = root_28$1(); + Copy(sibling(first_child(fragment_20)), { size: 16 }); + append($$anchor, fragment_20); + }, + $$slots: { default: true } + }); + reset(div_25); + append($$anchor, fragment_19); + }, + $$slots: { default: true } + }); + var node_44 = sibling(node_41, 2); + Card(node_44, { + class: "bonus-card", + children: ($$anchor, $$slotProps) => { + var fragment_21 = root_29$1(); + Gift(first_child(fragment_21), { size: 42 }); + next(); + append($$anchor, fragment_21); + }, + $$slots: { default: true } + }); + Button(sibling(node_44, 2), { + variant: "outline", + class: "wide", + children: ($$anchor, $$slotProps) => { + var fragment_22 = root_30$1(); + Ticket(first_child(fragment_22), { size: 18 }); + next(); + append($$anchor, fragment_22); + }, + $$slots: { default: true } + }); + template_effect(() => { + set_text(text_17, logoEmoji); + set_text(text_18, title); + }); + append($$anchor, fragment_18); + }, + $$slots: { default: true } + }); + var node_48 = sibling(node_40, 2); + PhoneFrame(node_48, { + number: "7", + label: "Настройки", + children: ($$anchor, $$slotProps) => { + var fragment_23 = root_31$1(); + var div_26 = first_child(fragment_23); + var div_27 = child(div_26); + var div_28 = child(div_27); + var span_7 = child(div_28); + var text_19 = child(span_7, true); + reset(span_7); + reset(div_28); + var strong_6 = sibling(div_28); + var text_20 = child(strong_6, true); + reset(strong_6); + reset(div_27); + reset(div_26); + var node_49 = sibling(div_26, 2); + Card(node_49, { + class: "settings-profile", + children: ($$anchor, $$slotProps) => { + var fragment_24 = root_32$1(); + var div_29 = first_child(fragment_24); + var node_50 = child(div_29); + var consequent_3 = ($$anchor) => { + var img = root_33$1(); + template_effect(() => set_attribute(img, "src", previewAvatar)); + append($$anchor, img); + }; + var alternate_1 = ($$anchor) => { + User_round($$anchor, { size: 27 }); + }; + if_block(node_50, ($$render) => { + if (previewAvatar) $$render(consequent_3); + else $$render(alternate_1, -1); + }); + reset(div_29); + var div_30 = sibling(div_29, 2); + var strong_7 = child(div_30); + var text_21 = child(strong_7, true); + reset(strong_7); + var small_3 = sibling(strong_7, 2); + var text_22 = child(small_3, true); + reset(small_3); + var small_4 = sibling(small_3, 2); + var text_23 = child(small_4, true); + reset(small_4); + reset(div_30); + template_effect(() => { + set_text(text_21, previewTelegramName); + set_text(text_22, previewEmail); + set_text(text_23, previewTelegramId); + }); + append($$anchor, fragment_24); + }, + $$slots: { default: true } + }); + var div_31 = sibling(node_49, 2); + each(div_31, 5, () => settingsRows, index, ($$anchor, row) => { + var div_32 = root_35$1(); + var node_51 = child(div_32); + component(node_51, () => get(row)[0], ($$anchor, $$component) => { + $$component($$anchor, { size: 20 }); + }); + var span_8 = sibling(node_51, 2); + var strong_8 = child(span_8); + var text_24 = child(strong_8, true); + reset(strong_8); + var small_5 = sibling(strong_8); + var text_25 = child(small_5, true); + reset(small_5); + reset(span_8); + Arrow_right(sibling(span_8, 2), { size: 16 }); + reset(div_32); + template_effect(() => { + set_text(text_24, (get(row), untrack(() => get(row)[1]))); + set_text(text_25, (get(row), untrack(() => get(row)[2]))); + }); + append($$anchor, div_32); + }); + reset(div_31); + template_effect(() => { + set_text(text_19, logoEmoji); + set_text(text_20, title); + }); + append($$anchor, fragment_23); + }, + $$slots: { default: true } + }); + var node_53 = sibling(node_48, 2); + PhoneFrame(node_53, { + number: "8", + label: "Логин", + wide: true, + children: ($$anchor, $$slotProps) => { + var fragment_26 = root_36$1(); + var div_33 = first_child(fragment_26); + var div_34 = child(div_33); + var span_9 = child(div_34); + var text_26 = child(span_9, true); + reset(span_9); + reset(div_34); + var h1_1 = sibling(div_34, 2); + var text_27 = child(h1_1, true); + reset(h1_1); + next(); + reset(div_33); + Card(sibling(div_33, 2), { + class: "auth-card", + children: ($$anchor, $$slotProps) => { + var fragment_27 = root_37(); + var div_35 = sibling(first_child(fragment_27), 2); + Button(sibling(child(div_35)), { + variant: "outline", + children: ($$anchor, $$slotProps) => { + var fragment_28 = root_38$1(); + Mail(first_child(fragment_28), { size: 17 }); + next(); + append($$anchor, fragment_28); + }, + $$slots: { default: true } + }); + reset(div_35); + Button(sibling(div_35, 4), { + variant: "telegram", + class: "wide", + children: ($$anchor, $$slotProps) => { + var fragment_29 = root_39(); + Send(first_child(fragment_29), { size: 18 }); + next(); + append($$anchor, fragment_29); + }, + $$slots: { default: true } + }); + append($$anchor, fragment_27); + }, + $$slots: { default: true } + }); + next(2); + template_effect(() => { + set_text(text_26, logoEmoji); + set_text(text_27, title); + }); + append($$anchor, fragment_26); + }, + $$slots: { default: true } + }); + PhoneFrame(sibling(node_53, 2), { + number: "9", + label: "Подтверждение по коду", + wide: true, + children: ($$anchor, $$slotProps) => { + var fragment_30 = root_40(); + var node_60 = first_child(fragment_30); + BackTitle(node_60, { + title: "Подтверждение по email", + subtitle: "Мы отправили код на user@example.com" + }); + var div_36 = sibling(node_60, 2); + each(div_36, 4, () => [ + 1, + 2, + 3, + 4, + 5, + 6 + ], index, ($$anchor, digit) => { + var span_10 = root_41$1(); + var text_28 = child(span_10, true); + reset(span_10); + template_effect(() => set_text(text_28, digit)); + append($$anchor, span_10); + }); + reset(div_36); + var node_61 = sibling(div_36, 2); + Button(node_61, { + class: "wide bottom-action", + children: ($$anchor, $$slotProps) => { + next(); + append($$anchor, text("Подтвердить")); + }, + $$slots: { default: true } + }); + var button = sibling(node_61, 2); + Refresh_cw(child(button), { size: 15 }); + next(); + reset(button); + append($$anchor, fragment_30); + }, + $$slots: { default: true } + }); + reset(div); + template_effect(() => set_style(div, (deep_read_state(config()), untrack(() => `--accent: ${config().primaryColor || "#00fe7a"};`)))); + append($$anchor, div); + pop(); + } + //#endregion + //#region bot/app/web/frontend/src/App.svelte + var root_5 = /* @__PURE__ */ from_html(``); + var root_6 = /* @__PURE__ */ from_html(` `); + var root_4 = /* @__PURE__ */ from_html(`
Загрузка...
`); + var root_10 = /* @__PURE__ */ from_html(` `); + var root_12 = /* @__PURE__ */ from_html(`
`); + var root_8 = /* @__PURE__ */ from_html(`

Подтверждение по email

Мы отправили код на

`, 1); + var root_14 = /* @__PURE__ */ from_html(``); + var root_15 = /* @__PURE__ */ from_html(` `); + var root_18 = /* @__PURE__ */ from_html(` Код`, 1); + var root_17 = /* @__PURE__ */ from_html(`
Вход по email
`); + var root_21 = /* @__PURE__ */ from_html(` Войти через Telegram`, 1); + var root_22 = /* @__PURE__ */ from_html(`
`); + var root_19 = /* @__PURE__ */ from_html(`
Вход через Telegram
`); + var root_23 = /* @__PURE__ */ from_html(`
`); + var root_16 = /* @__PURE__ */ from_html(`
`, 1); + var root_13 = /* @__PURE__ */ from_html(`
Нет аккаунта? Создать
`); + var root_7 = /* @__PURE__ */ from_html(`
`); + var root_26 = /* @__PURE__ */ from_html(``); + var root_27 = /* @__PURE__ */ from_html(` `); + var root_25 = /* @__PURE__ */ from_html(`
`); + var root_29 = /* @__PURE__ */ from_html(``); + var root_30 = /* @__PURE__ */ from_html(` `); + var root_31 = /* @__PURE__ */ from_html(`

`); + var root_32 = /* @__PURE__ */ from_html(`
Использовано трафика
`, 1); + var root_33 = /* @__PURE__ */ from_html(` Установить и настроить`, 1); + var root_34 = /* @__PURE__ */ from_html(` Продлить`, 1); + var root_35 = /* @__PURE__ */ from_html(` Сменить тариф`, 1); + var root_28 = /* @__PURE__ */ from_html(`
`); + var root_38 = /* @__PURE__ */ from_html(``); + var root_41 = /* @__PURE__ */ from_html(`Далее `, 1); + var root_36 = /* @__PURE__ */ from_html(`

Выбор тарифа

Экраны тарифов пока сверстаны без подключения

`); + var root_45 = /* @__PURE__ */ from_html(` `); + var root_44 = /* @__PURE__ */ from_html(``); + var root_47 = /* @__PURE__ */ from_html(`Итого
К оплате
`, 1); + var root_49 = /* @__PURE__ */ from_html(``); + var root_52 = /* @__PURE__ */ from_html(` `, 1); + var root_42 = /* @__PURE__ */ from_html(`

Подписка

Выберите срок подписки

`); + var root_55 = /* @__PURE__ */ from_html(``); + var root_57 = /* @__PURE__ */ from_html(`Итого
К оплате
`, 1); + var root_58 = /* @__PURE__ */ from_html(``); + var root_59 = /* @__PURE__ */ from_html(` `, 1); + var root_53 = /* @__PURE__ */ from_html(`

Трафик

Выберите пакет трафика

`); + var root_62 = /* @__PURE__ */ from_html(``); + var root_65 = /* @__PURE__ */ from_html(`Далее `, 1); + var root_60 = /* @__PURE__ */ from_html(`

Смена тарифа

Остаток 12 дней будет пересчитан

`); + var root_68 = /* @__PURE__ */ from_html(`Копировать `, 1); + var root_67 = /* @__PURE__ */ from_html(`
Ваша реферальная ссылка
`, 1); + var root_69 = /* @__PURE__ */ from_html(`
Ваш бонус +7 дней за каждого друга

Друг получит +3 дня к подписке после регистрации.

`, 1); + var root_71 = /* @__PURE__ */ from_html(` Активировать`, 1); + var root_72 = /* @__PURE__ */ from_html(`

`); + var root_70 = /* @__PURE__ */ from_html(`
Промокод
`, 1); + var root_66 = /* @__PURE__ */ from_html(`
`); + var root_75 = /* @__PURE__ */ from_html(`Аватар пользователя`); + var root_74 = /* @__PURE__ */ from_html(`
`, 1); + var root_73 = /* @__PURE__ */ from_html(`
`); + var root_77 = /* @__PURE__ */ from_html(``); + var root_24 = /* @__PURE__ */ from_html(`
`); + var root_78 = /* @__PURE__ */ from_html(`
`); + var root_81 = /* @__PURE__ */ from_html(`
`); + var root_3 = /* @__PURE__ */ from_html(`
`); + function App($$anchor, $$props) { + push($$props, false); + const brandTitle = /* @__PURE__ */ mutable_source(); + const brandEmoji = /* @__PURE__ */ mutable_source(); + const accent = /* @__PURE__ */ mutable_source(); + const plans = /* @__PURE__ */ mutable_source(); + const methods = /* @__PURE__ */ mutable_source(); + const subscription = /* @__PURE__ */ mutable_source(); + const user = /* @__PURE__ */ mutable_source(); + const referral = /* @__PURE__ */ mutable_source(); + const userLanguage = /* @__PURE__ */ mutable_source(); + const telegramProfileName = /* @__PURE__ */ mutable_source(); + const profileEmail = /* @__PURE__ */ mutable_source(); + const profileTelegramId = /* @__PURE__ */ mutable_source(); + const profileAvatarUrl = /* @__PURE__ */ mutable_source(); + const TELEGRAM_LOGIN_WIDGET_URL = "https://telegram.org/js/telegram-widget.js?23"; + const TELEGRAM_LOGIN_WIDGET_RENDER_TIMEOUT_MS = 8e3; + const MANUAL_LOGOUT_FLAG_KEY = "rw_webapp_manual_logout"; + const LANGUAGE_LABELS = { + ru: "Русский", + en: "English", + de: "Deutsch", + es: "Español", + fr: "Français", + tr: "Türkçe", + uk: "Українська" + }; + const DEV_MOCK = { + config: { + title: "/minishop", + primaryColor: "#00fe7a", + logoUrl: "", + logoEmoji: "🫥", + apiBase: "/api", + supportUrl: "https://t.me/support", + privacyPolicyUrl: "https://example.com/privacy", + userAgreementUrl: "https://example.com/agreement", + currency: "RUB", + language: "ru", + emailAuthEnabled: true, + telegramLoginBotUsername: "preview_bot" + }, + data: { + ok: true, + user: { + id: 100200300, + username: "username", + email: "user@example.com", + email_verified: true, + telegram_id: 100200300, + telegram_linked: true, + telegram_photo_url: "", + first_name: "Preview", + language_code: "ru" + }, + subscription: { + active: true, + status: "ACTIVE", + remaining_text: "25 д. 8 ч.", + end_date_text: "24.05.2026", + days_left: 25, + config_link: "https://sub.example.com/sub/preview-token", + connect_url: "https://sub.example.com/connect/preview-token", + traffic_used: "18.4 GB", + traffic_limit: "100 GB", + traffic_used_bytes: 19756849561, + traffic_limit_bytes: 107374182400 + }, + plans: [ + { + months: 1, + price: 290, + currency: "RUB", + title: "1 месяц" + }, + { + months: 3, + price: 790, + currency: "RUB", + title: "3 месяца" + }, + { + months: 6, + price: 1490, + currency: "RUB", + title: "6 месяцев" + }, + { + months: 12, + price: 2690, + currency: "RUB", + title: "12 месяцев" + } + ], + payment_methods: [ + { + id: "yookassa", + name: "Карта" + }, + { + id: "platega_sbp", + name: "Telegram Pay" + }, + { + id: "cryptopay", + name: "Криптовалюта" + }, + { + id: "freekassa", + name: "Другие способы" + } + ], + referral: { + code: "ABCD1234", + bot_link: "https://t.me/preview_bot?start=ref_uABCD1234", + webapp_link: "https://minishop.app/ref/ABCD1234", + invited_count: 4, + purchased_count: 2, + bonus_details: [{ + months: 1, + title: "1 месяц", + inviter_days: 7, + friend_days: 3 + }, { + months: 3, + title: "3 месяца", + inviter_days: 14, + friend_days: 7 + }] + }, + settings: { + support_url: "https://t.me/support", + traffic_mode: false, + email_auth_enabled: true + } + } + }; + const demoTariffs = [ + { + id: "subscription", + title: "Подписка", + caption: "Безлимитный трафик", + details: "Идеально для постоянного использования", + icon: "infinity", + billing: "period" + }, + { + id: "traffic", + title: "Трафик", + caption: "Пакеты гигабайт", + details: "Платите только за нужный объем", + icon: "database", + billing: "traffic" + }, + { + id: "premium", + title: "Премиум", + caption: "Максимальная скорость", + details: "Приоритетные серверы и поддержка", + icon: "crown", + billing: "period" + } + ]; + const trafficPackages = [ + { + gb: 20, + price: 290 + }, + { + gb: 50, + price: 590 + }, + { + gb: 100, + price: 990 + }, + { + gb: 300, + price: 2190 + } + ]; + const changeTariffs = [ + { + ...demoTariffs[0], + recalculation: "Доплата 190 ₽" + }, + { + ...demoTariffs[1], + recalculation: "Доплата не требуется" + }, + { + ...demoTariffs[2], + recalculation: "Доплата 390 ₽" + } + ]; + const isPreviewBoard = new URLSearchParams(window.location.search).get("preview") === "all"; + const injectedConfig = readJsonScript("webapp-config"); + const isLocalShell = window.location.protocol === "file:" || [ + "", + "localhost", + "127.0.0.1" + ].includes(window.location.hostname); + const MOCK = !injectedConfig && isLocalShell ? DEV_MOCK : null; + const CFG = { + ...DEV_MOCK.config, + ...MOCK ? MOCK.config : {}, + ...injectedConfig || {} + }; + const tg = window.Telegram && window.Telegram.WebApp ? window.Telegram.WebApp : null; + let mode = /* @__PURE__ */ mutable_source(isPreviewBoard ? "preview" : "loading"); + let activeTab = /* @__PURE__ */ mutable_source("home"); + let screen = /* @__PURE__ */ mutable_source("home"); + let data = /* @__PURE__ */ mutable_source(isPreviewBoard ? structuredCloneSafe(DEV_MOCK.data) : null); + let selectedTariff = /* @__PURE__ */ mutable_source("subscription"); + let selectedChangeTariff = /* @__PURE__ */ mutable_source("traffic"); + let selectedPlan = /* @__PURE__ */ mutable_source(null); + let selectedTrafficPackage = /* @__PURE__ */ mutable_source(trafficPackages[2]); + let selectedMethod = /* @__PURE__ */ mutable_source(""); + let payBusy = /* @__PURE__ */ mutable_source(false); + let promoCode = /* @__PURE__ */ mutable_source(""); + let promoBusy = /* @__PURE__ */ mutable_source(false); + let promoStatus = /* @__PURE__ */ mutable_source(""); + let promoIsError = /* @__PURE__ */ mutable_source(false); + let toastText = /* @__PURE__ */ mutable_source(""); + let toastTimer = null; + let authMode = /* @__PURE__ */ mutable_source(CFG.emailAuthEnabled === false ? "telegram" : "email"); + let authStatus = /* @__PURE__ */ mutable_source(""); + let authIsError = /* @__PURE__ */ mutable_source(false); + let authBusy = /* @__PURE__ */ mutable_source(false); + let email = /* @__PURE__ */ mutable_source(""); + let pendingEmail = /* @__PURE__ */ mutable_source(""); + let emailCode = /* @__PURE__ */ mutable_source(""); + let emailAvatarUrl = /* @__PURE__ */ mutable_source(""); + let avatarHashToken = /* @__PURE__ */ mutable_source(""); + let loginTelegramNode = /* @__PURE__ */ mutable_source(); + let token = MOCK ? "local-preview" : localStorage.getItem("rw_webapp_token") || ""; + let csrfToken = MOCK ? "" : readCookie("rw_webapp_csrf") || ""; + let confirmTariffOpen = /* @__PURE__ */ mutable_source(false); + onMount(() => { + if (isPreviewBoard) return; + boot(); + }); + function readJsonScript(id) { + const node = document.getElementById(id); + if (!node || !node.textContent) return null; + try { + return JSON.parse(node.textContent); + } catch (error) { + console.warn(`Failed to parse JSON config from #${id}`, error); + return null; + } + } + function structuredCloneSafe(value) { + try { + return structuredClone(value); + } catch { + return JSON.parse(JSON.stringify(value)); + } + } + async function boot() { + set(mode, "loading"); + if (tg) try { + tg.ready(); + tg.expand(); + } catch {} + if (MOCK) { + await loadData(); + return; + } + const magicToken = readMagicLoginToken(); + if (magicToken && await finalizeMagicLogin(magicToken)) return; + if (isManuallyLoggedOut()) { + showLogin(); + return; + } + const widgetAuthData = readTelegramLoginWidgetAuthData(); + if (widgetAuthData && await finalizeTelegramAuth(widgetAuthData, "auth_data")) return; + if (tg?.initData) try { + if (await finalizeTelegramAuth(tg.initData, "init_data")) return; + } catch {} + if (token || csrfToken) try { + await loadData(); + return; + } catch { + clearToken(); + } + showLogin(); + } + async function loadData() { + const payload = await api("/me"); + if (!payload.ok) throw new Error(payload.error || "load_failed"); + set(data, payload); + set(selectedPlan, payload.plans?.[Math.min(1, payload.plans.length - 1)] || payload.plans?.[0] || null); + set(selectedMethod, payload.payment_methods?.[0]?.id || ""); + set(screen, "home"); + set(mode, "app"); + } + function showLogin() { + set(mode, "login"); + set(screen, "login"); + set(activeTab, "home"); + renderTelegramWidgetWhenNeeded(); + } + async function api(path, options = {}) { + if (MOCK) return mockApi(path, options); + const method = String(options.method || "GET").toUpperCase(); + const headers = { ...options.headers || {} }; + if (token) headers.Authorization = `Bearer ${token}`; + const csrf = csrfToken || readCookie("rw_webapp_csrf") || ""; + if (csrf && [ + "POST", + "PUT", + "PATCH", + "DELETE" + ].includes(method)) headers["X-CSRF-Token"] = csrf; + if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json"; + const response = await fetch(`${CFG.apiBase}${path}`, { + ...options, + headers + }); + const payload = await response.json().catch(() => ({})); + if (response.status === 401) { + clearToken(); + showLogin(); + } + return payload; + } + async function publicApi(path, payload = {}) { + if (MOCK) return mockApi(path, { + method: "POST", + body: JSON.stringify(payload) + }); + return (await fetch(`${CFG.apiBase}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + })).json(); + } + async function mockApi(path, options = {}) { + await new Promise((resolve) => window.setTimeout(resolve, 120)); + if (path === "/me") return structuredCloneSafe(DEV_MOCK.data); + if (path === "/auth/email/request") return { ok: true }; + if (path === "/auth/email/verify" || path === "/auth/email/magic") return { + ok: true, + token: "local-preview", + csrf_token: "local-preview-csrf" + }; + if (path === "/auth/token") return { + ok: true, + token: "local-preview", + csrf_token: "local-preview-csrf" + }; + if (path === "/promo/apply") return { + ok: true, + end_date_text: "31.05.2026" + }; + if (path === "/auth/logout") return { ok: true }; + if (path === "/payments" && String(options.method || "").toUpperCase() === "POST") return { + ok: true, + action: "open_link", + payment_url: "https://example.com/payment-preview", + payment_id: 10001 + }; + return { + ok: false, + error: "not_found" + }; + } + function readCookie(name) { + const prefix = `${name}=`; + const cookie = document.cookie.split("; ").find((part) => part.startsWith(prefix)); + return cookie ? decodeURIComponent(cookie.slice(prefix.length)) : ""; + } + function setToken(nextToken, nextCsrf = "") { + clearManualLogoutFlag(); + token = nextToken || ""; + csrfToken = nextCsrf || readCookie("rw_webapp_csrf") || ""; + if (token && !MOCK) localStorage.setItem("rw_webapp_token", token); + } + function clearToken() { + token = ""; + csrfToken = ""; + localStorage.removeItem("rw_webapp_token"); + } + function markManualLogout() { + try { + localStorage.setItem(MANUAL_LOGOUT_FLAG_KEY, "1"); + } catch {} + } + function clearManualLogoutFlag() { + try { + localStorage.removeItem(MANUAL_LOGOUT_FLAG_KEY); + } catch {} + } + function isManuallyLoggedOut() { + try { + return localStorage.getItem(MANUAL_LOGOUT_FLAG_KEY) === "1"; + } catch { + return false; + } + } + function readReferralParam() { + const params = new URLSearchParams(window.location.search); + const fromQuery = params.get("ref") || params.get("start") || params.get("start_param") || ""; + const fromTelegram = tg?.initDataUnsafe?.start_param || ""; + const value = String(fromTelegram || fromQuery || "").trim(); + if (value) { + localStorage.setItem("rw_webapp_referral", value); + return value; + } + return localStorage.getItem("rw_webapp_referral") || ""; + } + function readMagicLoginToken() { + return (new URLSearchParams(window.location.search).get("login_token") || "").trim() || null; + } + function readTelegramLoginWidgetAuthData() { + const params = new URLSearchParams(window.location.search); + const keys = [ + "id", + "first_name", + "last_name", + "username", + "photo_url", + "auth_date", + "hash" + ]; + const authData = {}; + let hasAuthValue = false; + keys.forEach((key) => { + if (!params.has(key)) return; + authData[key] = params.get(key) || ""; + hasAuthValue = true; + }); + if (!hasAuthValue || !authData.id || !authData.auth_date || !authData.hash) return null; + return authData; + } + function clearAuthQuery() { + const url = new URL(window.location.href); + [ + "login_token", + "login_purpose", + "id", + "first_name", + "last_name", + "username", + "photo_url", + "auth_date", + "hash" + ].forEach((key) => url.searchParams.delete(key)); + window.history?.replaceState?.({}, document.title, url.pathname + url.search + url.hash); + } + async function finalizeMagicLogin(loginToken) { + if (get(authBusy)) return false; + set(authBusy, true); + setAuthStatus("Проверяем вход..."); + try { + const payload = { token: loginToken }; + const referralParam = readReferralParam(); + if (referralParam) payload.referral_code = referralParam; + const response = await publicApi("/auth/email/magic", payload); + if (response.ok && response.token) { + setToken(response.token, response.csrf_token); + clearAuthQuery(); + await loadData(); + return true; + } + setAuthStatus("Не удалось подтвердить вход", true); + } catch { + setAuthStatus("Не удалось подтвердить вход", true); + } finally { + set(authBusy, false); + } + return false; + } + async function finalizeTelegramAuth(authData, source = "auth_data") { + if (get(authBusy)) return false; + set(authBusy, true); + setAuthStatus("Проверяем Telegram..."); + try { + const payload = source === "init_data" ? { init_data: authData } : { auth_data: authData }; + const referralParam = readReferralParam(); + if (referralParam) payload.referral_code = referralParam; + const response = await publicApi("/auth/token", payload); + if (response.ok && response.token) { + setToken(response.token, response.csrf_token); + clearAuthQuery(); + setAuthStatus(""); + await loadData(); + return true; + } + setAuthStatus(response.error === "banned" ? "Доступ запрещен" : "Telegram-вход не подтвержден", true); + } catch { + setAuthStatus("Telegram-вход сейчас недоступен", true); + } finally { + set(authBusy, false); + } + return false; + } + async function requestEmailCode() { + const normalized = get(email).trim().toLowerCase(); + if (!normalized || !normalized.includes("@")) { + setAuthStatus("Введите корректный email", true); + return; + } + set(authBusy, true); + setAuthStatus("Отправляем код..."); + try { + const payload = { + email: normalized, + language: "ru" + }; + const referralParam = readReferralParam(); + if (referralParam) payload.referral_code = referralParam; + const response = await publicApi("/auth/email/request", payload); + if (!response.ok) throw response; + set(pendingEmail, normalized); + set(emailCode, ""); + set(screen, "code"); + set(mode, "login"); + setAuthStatus(""); + } catch (error) { + setAuthStatus(emailError(error, "Не удалось отправить код"), true); + } finally { + set(authBusy, false); + } + } + async function verifyEmailCode() { + const code = get(emailCode).replace(/\D/g, "").slice(0, 6); + if (code.length !== 6) { + setAuthStatus("Введите 6 цифр из письма", true); + return; + } + set(authBusy, true); + setAuthStatus("Проверяем код..."); + try { + const payload = { + email: get(pendingEmail), + code + }; + const referralParam = readReferralParam(); + if (referralParam) payload.referral_code = referralParam; + const response = await publicApi("/auth/email/verify", payload); + if (!response.ok || !response.token) throw response; + setToken(response.token, response.csrf_token); + await loadData(); + setAuthStatus(""); + } catch (error) { + setAuthStatus(emailError(error, "Неверный код"), true); + } finally { + set(authBusy, false); + } + } + function emailError(error, fallback) { + if (error?.error === "rate_limited") return `Повторная отправка через ${error.retry_after || 60} сек.`; + if (error?.error === "invalid_email") return "Введите корректный email"; + if (error?.error === "expired_code") return "Код устарел"; + if (error?.error === "invalid_code" || error?.error === "too_many_attempts") return "Неверный код"; + return fallback; + } + function setAuthStatus(message, isError = false) { + set(authStatus, message); + set(authIsError, isError); + } + async function renderTelegramWidgetWhenNeeded() { + await tick(); + if (get(authMode) !== "telegram" || !get(loginTelegramNode)) return; + mutate(loginTelegramNode, get(loginTelegramNode).innerHTML = ""); + if (tg?.initData) return; + const botUsername = String(CFG.telegramLoginBotUsername || "").trim(); + if (!botUsername) { + setAuthStatus("Telegram Login Widget не настроен", true); + return; + } + window.onTelegramAuth = async (telegramUser) => { + await finalizeTelegramAuth(telegramUser, "auth_data"); + }; + appendTelegramLoginWidget(get(loginTelegramNode), botUsername, "onTelegramAuth", () => setAuthStatus("Telegram Login Widget сейчас недоступен", true)); + } + function appendTelegramLoginWidget(container, botUsername, callbackName, onUnavailable) { + const script = document.createElement("script"); + let unavailableShown = false; + const showUnavailable = () => { + if (unavailableShown) return; + unavailableShown = true; + onUnavailable(); + }; + script.async = true; + script.src = TELEGRAM_LOGIN_WIDGET_URL; + script.setAttribute("data-telegram-login", botUsername); + script.setAttribute("data-size", "large"); + script.setAttribute("data-userpic", "true"); + script.setAttribute("data-request-access", "write"); + script.setAttribute("data-onauth", `${callbackName}(user)`); + script.onerror = showUnavailable; + script.onload = () => { + window.setTimeout(() => { + if (!container.contains(script) || container.querySelector("iframe")) return; + showUnavailable(); + }, TELEGRAM_LOGIN_WIDGET_RENDER_TIMEOUT_MS); + }; + container.appendChild(script); + } + async function openTelegramLogin() { + set(authMode, "telegram"); + if (tg?.initData) { + await finalizeTelegramAuth(tg.initData, "init_data"); + return; + } + renderTelegramWidgetWhenNeeded(); + } + async function createPayment() { + if (!get(selectedPlan) || !get(selectedMethod) || get(payBusy)) return; + set(payBusy, true); + try { + const response = await api("/payments", { + method: "POST", + body: JSON.stringify({ + months: get(selectedPlan).months, + method: get(selectedMethod) + }) + }); + if (!response.ok || !response.payment_url) throw response; + showToast("Платеж создан"); + openExternalLink(response.payment_url); + } catch (error) { + showToast(error?.message || "Не удалось создать платеж"); + } finally { + set(payBusy, false); + } + } + function openExternalLink(url) { + if (!url) return; + if (tg?.openLink) tg.openLink(url); + else window.open(url, "_blank", "noopener"); + } + function openConnectLink() { + const url = get(subscription)?.connect_url || get(subscription)?.config_link; + if (!url) { + showToast("Ссылка для подключения пока недоступна"); + return; + } + openExternalLink(url); + } + async function copyText(value, success = "Скопировано") { + if (!value) { + showToast("Пока недоступно"); + return; + } + try { + await navigator.clipboard.writeText(value); + } catch { + const area = document.createElement("textarea"); + area.value = value; + document.body.appendChild(area); + area.select(); + document.execCommand("copy"); + area.remove(); + } + showToast(success); + } + async function applyPromo() { + const code = get(promoCode).trim(); + if (!code) { + set(promoStatus, "Введите промокод"); + set(promoIsError, true); + return; + } + set(promoBusy, true); + set(promoStatus, ""); + try { + const response = await api("/promo/apply", { + method: "POST", + body: JSON.stringify({ code }) + }); + if (!response.ok) throw response; + set(promoCode, ""); + set(promoStatus, response.end_date_text ? `Промокод активирован. Подписка до ${response.end_date_text}` : "Промокод активирован"); + set(promoIsError, false); + await loadData(); + } catch (error) { + set(promoStatus, error?.message || "Не удалось активировать промокод"); + set(promoIsError, true); + } finally { + set(promoBusy, false); + } + } + async function logout() { + markManualLogout(); + clearToken(); + try { + await publicApi("/auth/logout", { keepalive: true }); + } catch {} + showLogin(); + } + function showToast(message) { + set(toastText, message); + if (toastTimer) window.clearTimeout(toastTimer); + toastTimer = window.setTimeout(() => { + set(toastText, ""); + }, 2400); + } + function goHome() { + set(activeTab, "home"); + set(screen, "home"); + } + function goInvite() { + set(activeTab, "invite"); + set(screen, "invite"); + } + function goSettings() { + set(activeTab, "settings"); + set(screen, "settings"); + } + function methodMeta(method) { + const id = String(method?.id || "").toLowerCase(); + if (id.includes("yookassa") || id.includes("card")) return { + title: method.name || "Карта", + note: "Visa, Mastercard", + icon: Credit_card + }; + if (id.includes("platega") || id.includes("sbp")) return { + title: method.name || "СБП", + note: "Быстро и удобно", + icon: Send + }; + if (id.includes("crypto")) return { + title: method.name || "Криптовалюта", + note: "USDT, BTC, ETH", + icon: Wallet_cards + }; + if (id.includes("stars")) return { + title: "Telegram Stars", + note: "Оплата звездами", + icon: Zap + }; + return { + title: method.name || "Другие способы", + note: "ЮMoney, СБП и др.", + icon: Wallet_cards + }; + } + function formatMoney(value, currency = CFG.currency || "RUB") { + const numeric = Number(value || 0); + return `${Number.isInteger(numeric) ? String(numeric) : numeric.toFixed(2)} ${currency === "RUB" ? "₽" : currency}`; + } + function trafficPercent(sub) { + const used = Number(sub?.traffic_used_bytes || 0); + const limit = Number(sub?.traffic_limit_bytes || 0); + if (!limit || limit <= 0) return 100; + return Math.max(0, Math.min(100, Math.round(used / limit * 100))); + } + function trafficLabel(sub) { + if (!sub?.traffic_limit_bytes || Number(sub.traffic_limit_bytes) <= 0) return "Безлимитный трафик"; + return `${sub.traffic_used || "0 GB"} из ${sub.traffic_limit || "0 GB"}`; + } + function normalizedEmail(value) { + return String(value || "").trim().toLowerCase(); + } + function languageName(code) { + const key = String(code || "").trim().toLowerCase(); + if (!key) return "Русский"; + return LANGUAGE_LABELS[key] || key.toUpperCase(); + } + function telegramName(profile) { + const first = String(profile?.first_name || "").trim(); + const last = String(profile?.last_name || "").trim(); + if (first || last) return `${first} ${last}`.trim(); + const username = String(profile?.username || "").trim(); + if (username) return `@${username}`; + return "Telegram не привязан"; + } + function bytesToHex(buffer) { + return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join(""); + } + async function sha256Hex(value) { + const data = new TextEncoder().encode(value); + return bytesToHex(await window.crypto.subtle.digest("SHA-256", data)); + } + async function buildGravatarUrl(emailValue) { + if (!emailValue || !window.crypto?.subtle) return ""; + try { + return `https://www.gravatar.com/avatar/${await sha256Hex(emailValue)}?d=mp&s=160`; + } catch { + return ""; + } + } + function tariffIcon(id) { + if (id === "traffic") return Database; + if (id === "premium") return Crown; + return Zap; + } + legacy_pre_effect(() => {}, () => { + set(brandTitle, CFG.title || "/minishop"); + }); + legacy_pre_effect(() => {}, () => { + set(brandEmoji, CFG.logoEmoji || "🫥"); + }); + legacy_pre_effect(() => {}, () => { + set(accent, CFG.primaryColor || "#00fe7a"); + }); + legacy_pre_effect(() => get(data), () => { + set(plans, get(data)?.plans?.length ? get(data).plans : DEV_MOCK.data.plans); + }); + legacy_pre_effect(() => get(data), () => { + set(methods, get(data)?.payment_methods?.length ? get(data).payment_methods : []); + }); + legacy_pre_effect(() => get(data), () => { + set(subscription, get(data)?.subscription || DEV_MOCK.data.subscription); + }); + legacy_pre_effect(() => get(data), () => { + set(user, get(data)?.user || {}); + }); + legacy_pre_effect(() => get(data), () => { + set(referral, get(data)?.referral || DEV_MOCK.data.referral); + }); + legacy_pre_effect(() => get(user), () => { + set(userLanguage, languageName(get(user)?.language_code || CFG.language || "ru")); + }); + legacy_pre_effect(() => get(user), () => { + set(telegramProfileName, telegramName(get(user))); + }); + legacy_pre_effect(() => get(user), () => { + set(profileEmail, get(user)?.email || "Почта не привязана"); + }); + legacy_pre_effect(() => get(user), () => { + set(profileTelegramId, get(user)?.telegram_id ? `TG ID ${get(user).telegram_id}` : "TG ID не привязан"); + }); + legacy_pre_effect(() => (get(user), get(avatarHashToken)), () => { + const emailKey = normalizedEmail(get(user)?.email); + if (!emailKey) { + set(avatarHashToken, ""); + set(emailAvatarUrl, ""); + } else if (get(avatarHashToken) !== emailKey) { + set(avatarHashToken, emailKey); + buildGravatarUrl(emailKey).then((url) => { + if (get(avatarHashToken) === emailKey) set(emailAvatarUrl, url); + }); + } + }); + legacy_pre_effect(() => (get(user), get(emailAvatarUrl)), () => { + set(profileAvatarUrl, get(user)?.telegram_photo_url || get(emailAvatarUrl) || ""); + }); + legacy_pre_effect(() => (get(selectedPlan), get(plans)), () => { + if (!get(selectedPlan) && get(plans).length) set(selectedPlan, get(plans)[Math.min(1, get(plans).length - 1)]); + }); + legacy_pre_effect(() => (get(selectedMethod), get(methods)), () => { + if (!get(selectedMethod) && get(methods).length) set(selectedMethod, get(methods)[0].id); + }); + legacy_pre_effect_reset(); + init(); + var fragment = comment(); + head("150dgq4", ($$anchor) => { + deferred_template_effect(() => { + $document.title = get(brandTitle) ?? ""; + }); + }); + var node_1 = first_child(fragment); + var consequent = ($$anchor) => { + PreviewBoard($$anchor, { + get config() { + return CFG; + }, + get mockData() { + return untrack(() => DEV_MOCK.data); + } + }); + }; + var alternate_12 = ($$anchor) => { + var div = root_3(); + var node_2 = child(div); + var consequent_2 = ($$anchor) => { + var div_1 = root_4(); + var div_2 = child(div_1); + var node_3 = child(div_2); + var consequent_1 = ($$anchor) => { + var img = root_5(); + template_effect(() => set_attribute(img, "src", untrack(() => CFG.logoUrl))); + append($$anchor, img); + }; + var alternate = ($$anchor) => { + var span = root_6(); + var text = child(span, true); + reset(span); + template_effect(() => set_text(text, get(brandEmoji))); + append($$anchor, span); + }; + if_block(node_3, ($$render) => { + if (untrack(() => CFG.logoUrl)) $$render(consequent_1); + else $$render(alternate, -1); + }); + reset(div_2); + next(2); + reset(div_1); + append($$anchor, div_1); + }; + var consequent_9 = ($$anchor) => { + var div_3 = root_7(); + var node_4 = child(div_3); + var consequent_4 = ($$anchor) => { + var fragment_2 = root_8(); + var header = first_child(fragment_2); + var node_5 = child(header); + Button(node_5, { + variant: "icon", + size: "icon", + onclick: () => set(screen, "login"), + "aria-label": "Назад", + children: ($$anchor, $$slotProps) => { + Arrow_left($$anchor, { size: 19 }); + }, + $$slots: { default: true } + }); + var div_4 = sibling(node_5, 2); + var p = sibling(child(div_4), 2); + var strong = sibling(child(p)); + var text_1 = child(strong, true); + reset(strong); + reset(p); + reset(div_4); + next(2); + reset(header); + var div_5 = sibling(header, 2); + var label = child(div_5); + var input = child(label); + remove_input_defaults(input); + var span_1 = sibling(input, 2); + each(span_1, 4, () => untrack(() => Array.from({ length: 6 })), index, ($$anchor, _, index) => { + var span_2 = root_10(); + let classes; + var text_2 = child(span_2, true); + reset(span_2); + template_effect(() => { + classes = set_class(span_2, 1, "", null, classes, { filled: get(emailCode)[index] }); + set_text(text_2, (get(emailCode), untrack(() => get(emailCode)[index] || ""))); + }); + append($$anchor, span_2); + }); + reset(span_1); + reset(label); + var node_6 = sibling(label, 2); + Button(node_6, { + class: "wide", + onclick: verifyEmailCode, + get disabled() { + return get(authBusy); + }, + children: ($$anchor, $$slotProps) => { + next(); + append($$anchor, text("Подтвердить")); + }, + $$slots: { default: true } + }); + var node_7 = sibling(node_6, 2); + var consequent_3 = ($$anchor) => { + var div_6 = root_12(); + let classes_1; + var text_4 = child(div_6, true); + reset(div_6); + template_effect(() => { + classes_1 = set_class(div_6, 1, "status-line", null, classes_1, { error: get(authIsError) }); + set_text(text_4, get(authStatus)); + }); + append($$anchor, div_6); + }; + if_block(node_7, ($$render) => { + if (get(authStatus)) $$render(consequent_3); + }); + var button = sibling(node_7, 2); + Refresh_cw(child(button), { size: 15 }); + next(); + reset(button); + reset(div_5); + template_effect(() => { + set_text(text_1, get(pendingEmail)); + button.disabled = get(authBusy); + }); + bind_value(input, () => get(emailCode), ($$value) => set(emailCode, $$value)); + event("click", button, requestEmailCode); + append($$anchor, fragment_2); + }; + var alternate_4 = ($$anchor) => { + var div_7 = root_13(); + var div_8 = child(div_7); + var div_9 = child(div_8); + var node_9 = child(div_9); + var consequent_5 = ($$anchor) => { + var img_1 = root_14(); + template_effect(() => set_attribute(img_1, "src", untrack(() => CFG.logoUrl))); + append($$anchor, img_1); + }; + var alternate_1 = ($$anchor) => { + var span_3 = root_15(); + var text_5 = child(span_3, true); + reset(span_3); + template_effect(() => set_text(text_5, get(brandEmoji))); + append($$anchor, span_3); + }; + if_block(node_9, ($$render) => { + if (untrack(() => CFG.logoUrl)) $$render(consequent_5); + else $$render(alternate_1, -1); + }); + reset(div_9); + var h1 = sibling(div_9, 2); + var text_6 = child(h1, true); + reset(h1); + next(2); + reset(div_8); + Card(sibling(div_8, 2), { + class: "auth-card", + children: ($$anchor, $$slotProps) => { + var fragment_4 = root_16(); + var div_10 = first_child(fragment_4); + var button_1 = child(div_10); + let classes_2; + var button_2 = sibling(button_1, 2); + let classes_3; + reset(div_10); + var node_11 = sibling(div_10, 2); + var consequent_6 = ($$anchor) => { + var div_11 = root_17(); + var div_12 = sibling(child(div_11), 2); + var node_12 = child(div_12); + Input(node_12, { + type: "email", + placeholder: "Email", + autocomplete: "email", + get value() { + return get(email); + }, + set value($$value) { + set(email, $$value); + }, + $$legacy: true + }); + Button(sibling(node_12, 2), { + variant: "outline", + onclick: requestEmailCode, + get disabled() { + return get(authBusy); + }, + children: ($$anchor, $$slotProps) => { + var fragment_5 = root_18(); + Mail(first_child(fragment_5), { size: 18 }); + next(); + append($$anchor, fragment_5); + }, + $$slots: { default: true } + }); + reset(div_12); + reset(div_11); + append($$anchor, div_11); + }; + var alternate_3 = ($$anchor) => { + var div_13 = root_19(); + var node_15 = sibling(child(div_13), 2); + var consequent_7 = ($$anchor) => { + Button($$anchor, { + variant: "telegram", + onclick: openTelegramLogin, + get disabled() { + return get(authBusy); + }, + children: ($$anchor, $$slotProps) => { + var fragment_7 = root_21(); + Send(first_child(fragment_7), { size: 19 }); + next(); + append($$anchor, fragment_7); + }, + $$slots: { default: true } + }); + }; + var alternate_2 = ($$anchor) => { + var div_14 = root_22(); + bind_this(div_14, ($$value) => set(loginTelegramNode, $$value), () => get(loginTelegramNode)); + append($$anchor, div_14); + }; + if_block(node_15, ($$render) => { + if (untrack(() => tg?.initData)) $$render(consequent_7); + else $$render(alternate_2, -1); + }); + reset(div_13); + append($$anchor, div_13); + }; + if_block(node_11, ($$render) => { + if (get(authMode) === "email") $$render(consequent_6); + else $$render(alternate_3, -1); + }); + var node_17 = sibling(node_11, 2); + var consequent_8 = ($$anchor) => { + var div_15 = root_23(); + let classes_4; + var text_7 = child(div_15, true); + reset(div_15); + template_effect(() => { + classes_4 = set_class(div_15, 1, "status-line", null, classes_4, { error: get(authIsError) }); + set_text(text_7, get(authStatus)); + }); + append($$anchor, div_15); + }; + if_block(node_17, ($$render) => { + if (get(authStatus)) $$render(consequent_8); + }); + template_effect(() => { + classes_2 = set_class(button_1, 1, "", null, classes_2, { active: get(authMode) === "email" }); + classes_3 = set_class(button_2, 1, "", null, classes_3, { active: get(authMode) === "telegram" }); + }); + event("click", button_1, () => set(authMode, "email")); + event("click", button_2, openTelegramLogin); + append($$anchor, fragment_4); + }, + $$slots: { default: true } + }); + next(2); + reset(div_7); + template_effect(() => set_text(text_6, get(brandTitle))); + append($$anchor, div_7); + }; + if_block(node_4, ($$render) => { + if (get(screen) === "code") $$render(consequent_4); + else $$render(alternate_4, -1); + }); + reset(div_3); + append($$anchor, div_3); + }; + var alternate_11 = ($$anchor) => { + var div_16 = root_24(); + var node_18 = child(div_16); + var consequent_11 = ($$anchor) => { + var header_1 = root_25(); + var div_17 = child(header_1); + var div_18 = child(div_17); + var node_19 = child(div_18); + var consequent_10 = ($$anchor) => { + var img_2 = root_26(); + template_effect(() => set_attribute(img_2, "src", untrack(() => CFG.logoUrl))); + append($$anchor, img_2); + }; + var alternate_5 = ($$anchor) => { + var span_4 = root_27(); + var text_8 = child(span_4, true); + reset(span_4); + template_effect(() => set_text(text_8, get(brandEmoji))); + append($$anchor, span_4); + }; + if_block(node_19, ($$render) => { + if (untrack(() => CFG.logoUrl)) $$render(consequent_10); + else $$render(alternate_5, -1); + }); + reset(div_18); + var strong_1 = sibling(div_18, 2); + var text_9 = child(strong_1, true); + reset(strong_1); + reset(div_17); + reset(header_1); + template_effect(() => set_text(text_9, get(brandTitle))); + append($$anchor, header_1); + }; + if_block(node_18, ($$render) => { + if (get(screen) === "invite" || get(screen) === "settings") $$render(consequent_11); + }); + var node_20 = sibling(node_18, 2); + var consequent_13 = ($$anchor) => { + var main = root_28(); + var div_19 = child(main); + var div_20 = child(div_19); + var node_21 = child(div_20); + var consequent_12 = ($$anchor) => { + var img_3 = root_29(); + template_effect(() => set_attribute(img_3, "src", untrack(() => CFG.logoUrl))); + append($$anchor, img_3); + }; + var alternate_6 = ($$anchor) => { + var span_5 = root_30(); + var text_10 = child(span_5, true); + reset(span_5); + template_effect(() => set_text(text_10, get(brandEmoji))); + append($$anchor, span_5); + }; + if_block(node_21, ($$render) => { + if (untrack(() => CFG.logoUrl)) $$render(consequent_12); + else $$render(alternate_6, -1); + }); + reset(div_20); + var h1_1 = sibling(div_20, 2); + var text_11 = child(h1_1, true); + reset(h1_1); + reset(div_19); + var div_21 = sibling(div_19, 2); + var node_22 = child(div_21); + Card(node_22, { + class: "status-card", + children: ($$anchor, $$slotProps) => { + var div_22 = root_31(); + var node_23 = child(div_22); + Circle_check(node_23, { size: 23 }); + var div_23 = sibling(node_23, 2); + var h2 = child(div_23); + var text_12 = child(h2, true); + reset(h2); + var p_1 = sibling(h2, 2); + var text_13 = child(p_1, true); + reset(p_1); + reset(div_23); + reset(div_22); + template_effect(() => { + set_text(text_12, (get(subscription), untrack(() => get(subscription).active ? "Подписка активна" : "Подписка не активна"))); + set_text(text_13, (get(subscription), untrack(() => get(subscription).end_date_text ? `до ${get(subscription).end_date_text}` : get(subscription).remaining_text))); + }); + append($$anchor, div_22); + }, + $$slots: { default: true } + }); + var node_24 = sibling(node_22, 2); + Card(node_24, { + children: ($$anchor, $$slotProps) => { + var fragment_8 = root_32(); + var div_24 = first_child(fragment_8); + var strong_2 = sibling(child(div_24), 2); + var text_14 = child(strong_2, true); + reset(strong_2); + reset(div_24); + var div_25 = sibling(div_24, 2); + var span_6 = child(div_25); + reset(div_25); + var div_26 = sibling(div_25, 2); + var text_15 = child(div_26); + reset(div_26); + template_effect(($0, $1, $2) => { + set_text(text_14, $0); + set_style(span_6, $1); + set_text(text_15, `${$2 ?? ""}%`); + }, [ + () => (get(subscription), untrack(() => trafficLabel(get(subscription)))), + () => (get(subscription), untrack(() => `width: ${trafficPercent(get(subscription))}%`)), + () => (get(subscription), untrack(() => trafficPercent(get(subscription)))) + ]); + append($$anchor, fragment_8); + }, + $$slots: { default: true } + }); + var div_27 = sibling(node_24, 2); + var node_25 = child(div_27); + Button(node_25, { + class: "wide", + onclick: openConnectLink, + children: ($$anchor, $$slotProps) => { + var fragment_9 = root_33(); + Download(first_child(fragment_9), { size: 18 }); + next(); + append($$anchor, fragment_9); + }, + $$slots: { default: true } + }); + var node_27 = sibling(node_25, 2); + Button(node_27, { + variant: "secondary", + class: "wide", + onclick: () => set(screen, "payment"), + children: ($$anchor, $$slotProps) => { + var fragment_10 = root_34(); + Refresh_cw(first_child(fragment_10), { size: 18 }); + next(); + append($$anchor, fragment_10); + }, + $$slots: { default: true } + }); + Button(sibling(node_27, 2), { + variant: "secondary", + class: "wide", + onclick: () => set(screen, "change-tariff"), + children: ($$anchor, $$slotProps) => { + var fragment_11 = root_35(); + Repeat_2(first_child(fragment_11), { size: 18 }); + next(); + append($$anchor, fragment_11); + }, + $$slots: { default: true } + }); + reset(div_27); + reset(div_21); + reset(main); + template_effect(() => set_text(text_11, get(brandTitle))); + append($$anchor, main); + }; + var consequent_15 = ($$anchor) => { + var main_1 = root_36(); + var header_2 = child(main_1); + Button(child(header_2), { + variant: "icon", + size: "icon", + onclick: goHome, + "aria-label": "Назад", + children: ($$anchor, $$slotProps) => { + Arrow_left($$anchor, { size: 19 }); + }, + $$slots: { default: true } + }); + next(4); + reset(header_2); + var div_28 = sibling(header_2, 2); + each(div_28, 5, () => demoTariffs, index, ($$anchor, tariff) => { + var button_3 = root_38(); + let classes_5; + var span_7 = child(button_3); + component(child(span_7), () => tariffIcon(get(tariff).id), ($$anchor, $$component) => { + $$component($$anchor, { size: 25 }); + }); + reset(span_7); + var span_8 = sibling(span_7, 2); + var strong_3 = child(span_8); + var text_16 = child(strong_3, true); + reset(strong_3); + var small = sibling(strong_3, 2); + var text_17 = child(small, true); + reset(small); + var em = sibling(small, 2); + var text_18 = child(em, true); + reset(em); + reset(span_8); + var node_33 = sibling(span_8, 2); + var consequent_14 = ($$anchor) => { + Circle_check($$anchor, { size: 22 }); + }; + var alternate_7 = ($$anchor) => { + Circle($$anchor, { size: 22 }); + }; + if_block(node_33, ($$render) => { + if (get(selectedTariff), get(tariff), untrack(() => get(selectedTariff) === get(tariff).id)) $$render(consequent_14); + else $$render(alternate_7, -1); + }); + reset(button_3); + template_effect(() => { + classes_5 = set_class(button_3, 1, "select-card", null, classes_5, { active: get(selectedTariff) === get(tariff).id }); + set_text(text_16, (get(tariff), untrack(() => get(tariff).title))); + set_text(text_17, (get(tariff), untrack(() => get(tariff).caption))); + set_text(text_18, (get(tariff), untrack(() => get(tariff).details))); + }); + event("click", button_3, () => set(selectedTariff, get(tariff).id)); + append($$anchor, button_3); + }); + reset(div_28); + Button(sibling(div_28, 2), { + class: "wide bottom-action", + onclick: () => showToast("Тарифы пока не подключены"), + children: ($$anchor, $$slotProps) => { + next(); + var fragment_15 = root_41(); + Arrow_right(sibling(first_child(fragment_15)), { size: 18 }); + append($$anchor, fragment_15); + }, + $$slots: { default: true } + }); + reset(main_1); + append($$anchor, main_1); + }; + var consequent_19 = ($$anchor) => { + var main_2 = root_42(); + var header_3 = child(main_2); + Button(child(header_3), { + variant: "icon", + size: "icon", + onclick: goHome, + "aria-label": "Назад", + children: ($$anchor, $$slotProps) => { + Arrow_left($$anchor, { size: 19 }); + }, + $$slots: { default: true } + }); + next(4); + reset(header_3); + var div_29 = sibling(header_3, 2); + each(div_29, 5, () => get(plans), index, ($$anchor, plan) => { + var button_4 = root_44(); + let classes_6; + var strong_4 = child(button_4); + var text_19 = child(strong_4, true); + reset(strong_4); + var span_9 = sibling(strong_4, 2); + var text_20 = child(span_9, true); + reset(span_9); + var node_37 = sibling(span_9, 2); + var consequent_16 = ($$anchor) => { + var small_1 = root_45(); + var text_21 = child(small_1); + reset(small_1); + template_effect(($0) => set_text(text_21, `${$0 ?? ""}/мес`), [() => (get(plan), untrack(() => formatMoney(get(plan).price / get(plan).months, get(plan).currency)))]); + append($$anchor, small_1); + }; + if_block(node_37, ($$render) => { + if (get(plan), untrack(() => get(plan).months > 1)) $$render(consequent_16); + }); + var node_38 = sibling(node_37, 2); + var consequent_17 = ($$anchor) => { + Circle_check($$anchor, { size: 18 }); + }; + if_block(node_38, ($$render) => { + if (get(selectedPlan), get(plan), untrack(() => get(selectedPlan)?.months === get(plan).months)) $$render(consequent_17); + }); + reset(button_4); + template_effect(($0) => { + classes_6 = set_class(button_4, 1, "period-card", null, classes_6, { active: get(selectedPlan)?.months === get(plan).months }); + set_text(text_19, (get(plan), untrack(() => get(plan).title))); + set_text(text_20, $0); + }, [() => (get(plan), untrack(() => formatMoney(get(plan).price, get(plan).currency)))]); + event("click", button_4, () => set(selectedPlan, get(plan))); + append($$anchor, button_4); + }); + reset(div_29); + var node_39 = sibling(div_29, 2); + Card(node_39, { + class: "total-card", + children: ($$anchor, $$slotProps) => { + var fragment_18 = root_47(); + var strong_5 = sibling(first_child(fragment_18), 2); + var text_22 = child(strong_5, true); + reset(strong_5); + template_effect(($0) => set_text(text_22, $0), [() => (get(selectedPlan), untrack(() => get(selectedPlan) ? formatMoney(get(selectedPlan).price, get(selectedPlan).currency) : "..."))]); + append($$anchor, fragment_18); + }, + $$slots: { default: true } + }); + var div_30 = sibling(node_39, 2); + var node_40 = child(div_30); + var consequent_18 = ($$anchor) => { + var fragment_19 = comment(); + each(first_child(fragment_19), 1, () => get(methods), index, ($$anchor, method) => { + const meta = /* @__PURE__ */ derived_safe_equal(() => (get(method), untrack(() => methodMeta(get(method))))); + var button_5 = root_49(); + let classes_7; + var node_42 = child(button_5); + component(node_42, () => get(meta).icon, ($$anchor, $$component) => { + $$component($$anchor, { size: 19 }); + }); + var span_10 = sibling(node_42, 2); + var strong_6 = child(span_10); + var text_23 = child(strong_6, true); + reset(strong_6); + var small_2 = sibling(strong_6, 2); + var text_24 = child(small_2, true); + reset(small_2); + reset(span_10); + reset(button_5); + template_effect(() => { + classes_7 = set_class(button_5, 1, "method-card", null, classes_7, { active: get(selectedMethod) === get(method).id }); + set_text(text_23, (deep_read_state(get(meta)), untrack(() => get(meta).title))); + set_text(text_24, (deep_read_state(get(meta)), untrack(() => get(meta).note))); + }); + event("click", button_5, () => set(selectedMethod, get(method).id)); + append($$anchor, button_5); + }); + append($$anchor, fragment_19); + }; + var alternate_8 = ($$anchor) => { + Card($$anchor, { + class: "empty-card", + children: ($$anchor, $$slotProps) => { + next(); + append($$anchor, text("Способы оплаты пока не настроены")); + }, + $$slots: { default: true } + }); + }; + if_block(node_40, ($$render) => { + if (get(methods), untrack(() => get(methods).length)) $$render(consequent_18); + else $$render(alternate_8, -1); + }); + reset(div_30); + var node_43 = sibling(div_30, 2); + { + let $0 = /* @__PURE__ */ derived_safe_equal(() => (get(methods), get(payBusy), untrack(() => !get(methods).length || get(payBusy)))); + Button(node_43, { + class: "wide bottom-action", + onclick: createPayment, + get disabled() { + return get($0); + }, + children: ($$anchor, $$slotProps) => { + next(); + var fragment_21 = root_52(); + var text_26 = first_child(fragment_21); + Lock_keyhole(sibling(text_26), { size: 17 }); + template_effect(($0) => set_text(text_26, `Оплатить ${$0 ?? ""} `), [() => (get(selectedPlan), untrack(() => get(selectedPlan) ? formatMoney(get(selectedPlan).price, get(selectedPlan).currency) : ""))]); + append($$anchor, fragment_21); + }, + $$slots: { default: true } + }); + } + reset(main_2); + append($$anchor, main_2); + }; + var consequent_21 = ($$anchor) => { + var main_3 = root_53(); + var header_4 = child(main_3); + Button(child(header_4), { + variant: "icon", + size: "icon", + onclick: goHome, + "aria-label": "Назад", + children: ($$anchor, $$slotProps) => { + Arrow_left($$anchor, { size: 19 }); + }, + $$slots: { default: true } + }); + next(4); + reset(header_4); + var div_31 = sibling(header_4, 2); + each(div_31, 5, () => trafficPackages, index, ($$anchor, pack) => { + var button_6 = root_55(); + let classes_8; + var strong_7 = child(button_6); + var text_27 = child(strong_7); + reset(strong_7); + var span_11 = sibling(strong_7, 2); + var text_28 = child(span_11, true); + reset(span_11); + var small_3 = sibling(span_11, 2); + var text_29 = child(small_3); + reset(small_3); + var node_46 = sibling(small_3, 2); + var consequent_20 = ($$anchor) => { + Circle_check($$anchor, { size: 18 }); + }; + if_block(node_46, ($$render) => { + if (get(selectedTrafficPackage), get(pack), untrack(() => get(selectedTrafficPackage).gb === get(pack).gb)) $$render(consequent_20); + }); + reset(button_6); + template_effect(($0, $1) => { + classes_8 = set_class(button_6, 1, "period-card", null, classes_8, { active: get(selectedTrafficPackage).gb === get(pack).gb }); + set_text(text_27, `${(get(pack), untrack(() => get(pack).gb)) ?? ""} ГБ`); + set_text(text_28, $0); + set_text(text_29, `${$1 ?? ""}/ГБ`); + }, [() => (get(pack), untrack(() => formatMoney(get(pack).price))), () => (get(pack), untrack(() => formatMoney(get(pack).price / get(pack).gb)))]); + event("click", button_6, () => set(selectedTrafficPackage, get(pack))); + append($$anchor, button_6); + }); + reset(div_31); + var node_47 = sibling(div_31, 2); + Card(node_47, { + class: "total-card", + children: ($$anchor, $$slotProps) => { + var fragment_24 = root_57(); + var strong_8 = sibling(first_child(fragment_24), 2); + var text_30 = child(strong_8, true); + reset(strong_8); + template_effect(($0) => set_text(text_30, $0), [() => (get(selectedTrafficPackage), untrack(() => formatMoney(get(selectedTrafficPackage).price)))]); + append($$anchor, fragment_24); + }, + $$slots: { default: true } + }); + var div_32 = sibling(node_47, 2); + each(div_32, 5, () => untrack(() => DEV_MOCK.data.payment_methods), index, ($$anchor, method) => { + const meta = /* @__PURE__ */ derived_safe_equal(() => (get(method), untrack(() => methodMeta(get(method))))); + var button_7 = root_58(); + var node_48 = child(button_7); + component(node_48, () => get(meta).icon, ($$anchor, $$component) => { + $$component($$anchor, { size: 19 }); + }); + var span_12 = sibling(node_48, 2); + var strong_9 = child(span_12); + var text_31 = child(strong_9, true); + reset(strong_9); + var small_4 = sibling(strong_9, 2); + var text_32 = child(small_4, true); + reset(small_4); + reset(span_12); + reset(button_7); + template_effect(() => { + set_text(text_31, (deep_read_state(get(meta)), untrack(() => get(meta).title))); + set_text(text_32, (deep_read_state(get(meta)), untrack(() => get(meta).note))); + }); + append($$anchor, button_7); + }); + reset(div_32); + Button(sibling(div_32, 2), { + class: "wide bottom-action", + onclick: () => showToast("Пакеты трафика пока не подключены"), + children: ($$anchor, $$slotProps) => { + next(); + var fragment_25 = root_59(); + var text_33 = first_child(fragment_25); + Lock_keyhole(sibling(text_33), { size: 17 }); + template_effect(($0) => set_text(text_33, `Оплатить ${$0 ?? ""} `), [() => (get(selectedTrafficPackage), untrack(() => formatMoney(get(selectedTrafficPackage).price)))]); + append($$anchor, fragment_25); + }, + $$slots: { default: true } + }); + reset(main_3); + append($$anchor, main_3); + }; + var consequent_23 = ($$anchor) => { + var main_4 = root_60(); + var header_5 = child(main_4); + Button(child(header_5), { + variant: "icon", + size: "icon", + onclick: goHome, + "aria-label": "Назад", + children: ($$anchor, $$slotProps) => { + Arrow_left($$anchor, { size: 19 }); + }, + $$slots: { default: true } + }); + next(4); + reset(header_5); + var div_33 = sibling(header_5, 2); + each(div_33, 5, () => changeTariffs, index, ($$anchor, tariff) => { + var button_8 = root_62(); + let classes_9; + var span_13 = child(button_8); + var strong_10 = child(span_13); + var text_34 = child(strong_10, true); + reset(strong_10); + var small_5 = sibling(strong_10, 2); + var text_35 = child(small_5, true); + reset(small_5); + reset(span_13); + var em_1 = sibling(span_13, 2); + var text_36 = child(em_1, true); + reset(em_1); + var node_52 = sibling(em_1, 2); + var consequent_22 = ($$anchor) => { + Circle_check($$anchor, { size: 20 }); + }; + var alternate_9 = ($$anchor) => { + Circle($$anchor, { size: 20 }); + }; + if_block(node_52, ($$render) => { + if (get(selectedChangeTariff), get(tariff), untrack(() => get(selectedChangeTariff) === get(tariff).id)) $$render(consequent_22); + else $$render(alternate_9, -1); + }); + reset(button_8); + template_effect(() => { + classes_9 = set_class(button_8, 1, "select-card", null, classes_9, { active: get(selectedChangeTariff) === get(tariff).id }); + set_text(text_34, (get(tariff), untrack(() => get(tariff).title))); + set_text(text_35, (get(tariff), untrack(() => get(tariff).caption))); + set_text(text_36, (get(tariff), untrack(() => get(tariff).recalculation))); + }); + event("click", button_8, () => set(selectedChangeTariff, get(tariff).id)); + append($$anchor, button_8); + }); + reset(div_33); + Button(sibling(div_33, 2), { + class: "wide bottom-action", + onclick: () => get(selectedChangeTariff) === "traffic" ? set(confirmTariffOpen, true) : showToast("Оплата смены тарифа пока не подключена"), + children: ($$anchor, $$slotProps) => { + next(); + var fragment_29 = root_65(); + Arrow_right(sibling(first_child(fragment_29)), { size: 18 }); + append($$anchor, fragment_29); + }, + $$slots: { default: true } + }); + reset(main_4); + append($$anchor, main_4); + }; + var consequent_25 = ($$anchor) => { + var main_5 = root_66(); + var node_55 = child(main_5); + Card(node_55, { + children: ($$anchor, $$slotProps) => { + var fragment_30 = root_67(); + var div_34 = sibling(first_child(fragment_30), 2); + var code_1 = child(div_34); + var text_37 = child(code_1, true); + reset(code_1); + Button(sibling(code_1, 2), { + onclick: () => copyText(get(referral).webapp_link || get(referral).bot_link, "Ссылка скопирована"), + children: ($$anchor, $$slotProps) => { + next(); + var fragment_31 = root_68(); + Copy(sibling(first_child(fragment_31)), { size: 17 }); + append($$anchor, fragment_31); + }, + $$slots: { default: true } + }); + reset(div_34); + template_effect(() => set_text(text_37, (get(referral), untrack(() => get(referral).webapp_link || get(referral).bot_link || "Ссылка пока недоступна")))); + append($$anchor, fragment_30); + }, + $$slots: { default: true } + }); + var node_58 = sibling(node_55, 2); + Card(node_58, { + class: "bonus-card", + children: ($$anchor, $$slotProps) => { + var fragment_32 = root_69(); + Gift(first_child(fragment_32), { size: 42 }); + next(2); + append($$anchor, fragment_32); + }, + $$slots: { default: true } + }); + Card(sibling(node_58, 2), { + children: ($$anchor, $$slotProps) => { + var fragment_33 = root_70(); + var div_35 = sibling(first_child(fragment_33), 2); + var node_61 = child(div_35); + Input(node_61, { + placeholder: "PROMO2026", + get value() { + return get(promoCode); + }, + set value($$value) { + set(promoCode, $$value); + }, + $$legacy: true + }); + Button(sibling(node_61, 2), { + variant: "outline", + onclick: applyPromo, + get disabled() { + return get(promoBusy); + }, + children: ($$anchor, $$slotProps) => { + var fragment_34 = root_71(); + Ticket(first_child(fragment_34), { size: 17 }); + next(); + append($$anchor, fragment_34); + }, + $$slots: { default: true } + }); + reset(div_35); + var node_64 = sibling(div_35, 2); + var consequent_24 = ($$anchor) => { + var p_2 = root_72(); + let classes_10; + var text_38 = child(p_2, true); + reset(p_2); + template_effect(() => { + classes_10 = set_class(p_2, 1, "status-line", null, classes_10, { error: get(promoIsError) }); + set_text(text_38, get(promoStatus)); + }); + append($$anchor, p_2); + }; + if_block(node_64, ($$render) => { + if (get(promoStatus)) $$render(consequent_24); + }); + append($$anchor, fragment_33); + }, + $$slots: { default: true } + }); + reset(main_5); + append($$anchor, main_5); + }; + var consequent_27 = ($$anchor) => { + var main_6 = root_73(); + var node_65 = child(main_6); + Card(node_65, { + class: "settings-profile", + children: ($$anchor, $$slotProps) => { + var fragment_35 = root_74(); + var div_36 = first_child(fragment_35); + var node_66 = child(div_36); + var consequent_26 = ($$anchor) => { + var img_4 = root_75(); + template_effect(() => set_attribute(img_4, "src", get(profileAvatarUrl))); + append($$anchor, img_4); + }; + var alternate_10 = ($$anchor) => { + User_round($$anchor, { size: 30 }); + }; + if_block(node_66, ($$render) => { + if (get(profileAvatarUrl)) $$render(consequent_26); + else $$render(alternate_10, -1); + }); + reset(div_36); + var div_37 = sibling(div_36, 2); + var strong_11 = child(div_37); + var text_39 = child(strong_11, true); + reset(strong_11); + var small_6 = sibling(strong_11, 2); + var text_40 = child(small_6, true); + reset(small_6); + var small_7 = sibling(small_6, 2); + var text_41 = child(small_7, true); + reset(small_7); + reset(div_37); + template_effect(() => { + set_text(text_39, get(telegramProfileName)); + set_text(text_40, get(profileEmail)); + set_text(text_41, get(profileTelegramId)); + }); + append($$anchor, fragment_35); + }, + $$slots: { default: true } + }); + var div_38 = sibling(node_65, 2); + var button_9 = child(div_38); + var node_67 = child(button_9); + Earth(node_67, { size: 21 }); + var span_14 = sibling(node_67, 2); + var small_8 = sibling(child(span_14)); + var text_42 = child(small_8, true); + reset(small_8); + reset(span_14); + Arrow_right(sibling(span_14, 2), { size: 17 }); + reset(button_9); + var button_10 = sibling(button_9, 2); + var node_69 = child(button_10); + Send(node_69, { size: 21 }); + var span_15 = sibling(node_69, 2); + var small_9 = sibling(child(span_15)); + var text_43 = child(small_9, true); + reset(small_9); + reset(span_15); + Arrow_right(sibling(span_15, 2), { size: 17 }); + reset(button_10); + var button_11 = sibling(button_10, 2); + var node_71 = child(button_11); + Mail(node_71, { size: 21 }); + var span_16 = sibling(node_71, 2); + var small_10 = sibling(child(span_16)); + var text_44 = child(small_10, true); + reset(small_10); + reset(span_16); + Arrow_right(sibling(span_16, 2), { size: 17 }); + reset(button_11); + var button_12 = sibling(button_11, 2); + var node_73 = child(button_12); + User_round(node_73, { size: 21 }); + Arrow_right(sibling(node_73, 4), { size: 17 }); + reset(button_12); + reset(div_38); + reset(main_6); + template_effect(() => { + set_text(text_42, get(userLanguage)); + set_text(text_43, (get(user), untrack(() => get(user).telegram_linked ? `@${get(user).username || "username"}` : "Не привязан"))); + set_text(text_44, (get(user), untrack(() => get(user).email || "Не привязана"))); + }); + event("click", button_12, logout); + append($$anchor, main_6); + }; + if_block(node_20, ($$render) => { + if (get(screen) === "home") $$render(consequent_13); + else if (get(screen) === "tariff-select") $$render(consequent_15, 1); + else if (get(screen) === "payment") $$render(consequent_19, 2); + else if (get(screen) === "traffic-payment") $$render(consequent_21, 3); + else if (get(screen) === "change-tariff") $$render(consequent_23, 4); + else if (get(screen) === "invite") $$render(consequent_25, 5); + else if (get(screen) === "settings") $$render(consequent_27, 6); + }); + var node_75 = sibling(node_20, 2); + var consequent_28 = ($$anchor) => { + var nav = root_77(); + var button_13 = child(nav); + let classes_11; + House(child(button_13), { size: 21 }); + next(2); + reset(button_13); + var button_14 = sibling(button_13, 2); + let classes_12; + Gift(child(button_14), { size: 21 }); + next(2); + reset(button_14); + var button_15 = sibling(button_14, 2); + let classes_13; + Settings(child(button_15), { size: 21 }); + next(2); + reset(button_15); + reset(nav); + template_effect(() => { + classes_11 = set_class(button_13, 1, "", null, classes_11, { active: get(activeTab) === "home" }); + classes_12 = set_class(button_14, 1, "", null, classes_12, { active: get(activeTab) === "invite" }); + classes_13 = set_class(button_15, 1, "", null, classes_13, { active: get(activeTab) === "settings" }); + }); + event("click", button_13, goHome); + event("click", button_14, goInvite); + event("click", button_15, goSettings); + append($$anchor, nav); + }; + if_block(node_75, ($$render) => { + if (get(screen) === "home" || get(screen) === "invite" || get(screen) === "settings") $$render(consequent_28); + }); + reset(div_16); + append($$anchor, div_16); + }; + if_block(node_2, ($$render) => { + if (get(mode) === "loading") $$render(consequent_2); + else if (get(mode) === "login") $$render(consequent_9, 1); + else $$render(alternate_11, -1); + }); + var node_79 = sibling(node_2, 2); + Dialog(node_79, { + get open() { + return get(confirmTariffOpen); + }, + title: "Сменить тариф без доплаты?", + description: "Остаток 12 дней будет пересчитан по новому тарифу.", + onclose: () => set(confirmTariffOpen, false), + children: ($$anchor, $$slotProps) => { + var div_39 = root_78(); + var node_80 = child(div_39); + Button(node_80, { + onclick: () => { + set(confirmTariffOpen, false); + showToast("Смена тарифа пока не подключена"); + }, + children: ($$anchor, $$slotProps) => { + next(); + append($$anchor, text("Да, сменить")); + }, + $$slots: { default: true } + }); + Button(sibling(node_80, 2), { + variant: "secondary", + onclick: () => set(confirmTariffOpen, false), + children: ($$anchor, $$slotProps) => { + next(); + append($$anchor, text("Отмена")); + }, + $$slots: { default: true } + }); + reset(div_39); + append($$anchor, div_39); + }, + $$slots: { default: true } + }); + var node_82 = sibling(node_79, 2); + var consequent_29 = ($$anchor) => { + var div_40 = root_81(); + var text_47 = child(div_40, true); + reset(div_40); + template_effect(() => set_text(text_47, get(toastText))); + append($$anchor, div_40); + }; + if_block(node_82, ($$render) => { + if (get(toastText)) $$render(consequent_29); + }); + reset(div); + template_effect(() => set_style(div, `--accent: ${get(accent)};`)); + append($$anchor, div); + }; + if_block(node_1, ($$render) => { + if (isPreviewBoard) $$render(consequent); + else $$render(alternate_12, -1); + }); + append($$anchor, fragment); + pop(); + } + //#endregion + //#region bot/app/web/frontend/src/main.js + var target = document.getElementById("app"); + if (target) mount(App, { target }); + //#endregion +})(); diff --git a/config/settings.py b/config/settings.py index b6d1635..2e5ab5c 100644 --- a/config/settings.py +++ b/config/settings.py @@ -86,6 +86,7 @@ class WebAppSettings(BaseModel): title: str primary_color: str logo_url: Optional[str] + logo_emoji: str session_ttl_seconds: int session_secret: str webhook_secret_token: str @@ -329,6 +330,7 @@ class Settings(BaseSettings): WEBAPP_TITLE: str = Field(default="Моя подписка") WEBAPP_PRIMARY_COLOR: str = Field(default="#00fe7a") WEBAPP_LOGO_URL: Optional[str] = Field(default=None) + WEBAPP_LOGO_EMOJI: str = Field(default="🫥") WEBAPP_SESSION_SECRET: str = Field(default_factory=lambda: secrets.token_urlsafe(32)) WEBHOOK_SECRET_TOKEN: str = Field(default_factory=lambda: secrets.token_urlsafe(32)) WEBAPP_SESSION_TTL_SECONDS: int = Field(default=24 * 60 * 60) @@ -473,6 +475,7 @@ class Settings(BaseSettings): title=self.WEBAPP_TITLE, primary_color=self.WEBAPP_PRIMARY_COLOR, logo_url=self.WEBAPP_LOGO_URL, + logo_emoji=self.WEBAPP_LOGO_EMOJI, session_ttl_seconds=self.WEBAPP_SESSION_TTL_SECONDS, session_secret=self.WEBAPP_SESSION_SECRET, webhook_secret_token=self.WEBHOOK_SECRET_TOKEN, diff --git a/package-lock.json b/package-lock.json index 66ea61d..183d183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,51 @@ "packages": { "": { "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/cli": "4.2.4", + "bits-ui": "^2.18.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "esbuild": "^0.28.0", - "tailwindcss": "4.2.4" + "lucide-svelte": "^1.0.1", + "svelte": "^5.55.5", + "tailwind-merge": "^3.5.0", + "tailwindcss": "4.2.4", + "vite": "^8.0.10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -452,6 +494,45 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@internationalized/date": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz", + "integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -502,6 +583,35 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -829,6 +939,329 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", + "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tailwindcss/cli": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.4.tgz", @@ -1117,6 +1550,132 @@ "node": ">= 20" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bits-ui": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.0.tgz", + "integrity": "sha512-GLOBZRVy3hxNHIQ2MpD/+5aK9KcBFZRhUJtZ1UDABXdlVR4K6zFpgt4T+Rwuhf2sQzlc6yK1q/DprHPjwT4Pjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.35.1", + "svelte-toolbelt": "^0.10.6", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1127,6 +1686,13 @@ "node": ">=8" } }, + "node_modules/devalue": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz", + "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", @@ -1183,6 +1749,64 @@ "@esbuild/win32-x64": "0.28.0" } }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz", + "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1190,6 +1814,13 @@ "dev": true, "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "dev": true, + "license": "MIT" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1213,6 +1844,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -1496,6 +2137,33 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lucide-svelte": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-1.0.1.tgz", + "integrity": "sha512-WvzZgk0pqzgda+AErLvgWxHkfg/+GgUwqKMRHvzt0IqyMdmyEDzDCk3Z+Wo/3y753oIgx8u9Q4eUbWkghFa8Jg==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^3 || ^4 || ^5.0.0-next.42" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1516,6 +2184,25 @@ "node": ">=4" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -1523,6 +2210,17 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1543,6 +2241,94 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1553,6 +2339,83 @@ "node": ">=0.10.0" } }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/svelte": { + "version": "5.55.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.5.tgz", + "integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.4", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", @@ -1573,6 +2436,135 @@ "type": "opencollective", "url": "https://opencollective.com/webpack" } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 2ec6f43..2c5dbc7 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,22 @@ { "private": true, "scripts": { - "build:webapp:css": "tailwindcss -i ./bot/app/web/templates/subscription_webapp.tailwind.css -o ./bot/app/web/templates/subscription_webapp.css --minify", + "build:webapp:svelte": "vite build --config ./bot/app/web/frontend/vite.config.mjs", + "build:webapp:css": "npm run build:webapp:svelte", "build:webapp:js": "node ./scripts/build_subscription_webapp_js.mjs", - "build:webapp": "npm run build:webapp:css && npm run build:webapp:js" + "build:webapp": "npm run build:webapp:svelte && npm run build:webapp:js" }, "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/cli": "4.2.4", + "bits-ui": "^2.18.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "esbuild": "^0.28.0", - "tailwindcss": "4.2.4" + "lucide-svelte": "^1.0.1", + "svelte": "^5.55.5", + "tailwind-merge": "^3.5.0", + "tailwindcss": "4.2.4", + "vite": "^8.0.10" } }