From b531d4de3b671ebc672e2de5ffc226f7c786e3b3 Mon Sep 17 00:00:00 2001
From: 3252a8 <3252a8@proton.me>
Date: Sun, 31 May 2026 14:41:40 +0300
Subject: [PATCH] docs: add email preview demo section
---
docs-site/public/demo/demo-shell.js | 40 ++-
docs-site/src/lib/demoRoutes.mjs | 1 +
docs-site/src/lib/emailPreviews.mjs | 463 ++++++++++++++++++++++++++++
docs-site/src/pages/demo.astro | 342 ++++++++++++++++++++
4 files changed, 836 insertions(+), 10 deletions(-)
create mode 100644 docs-site/src/lib/emailPreviews.mjs
diff --git a/docs-site/public/demo/demo-shell.js b/docs-site/public/demo/demo-shell.js
index 5335cb2..542012f 100644
--- a/docs-site/public/demo/demo-shell.js
+++ b/docs-site/public/demo/demo-shell.js
@@ -11,6 +11,7 @@ const stateMocks = new Set([
"devices",
"notifications",
"auth",
+ "emails",
]);
const routeMocks = new Set([...stateMocks, "guides", "install"]);
const params = new URLSearchParams(window.location.search);
@@ -83,6 +84,7 @@ const routeFromParams = () => {
let initialRoute = routeFromParams();
const mockForRoute = (route) => {
const normalized = normalizePath(route);
+ if (normalized === "/emails") return "emails";
if (normalized === "/devices") return "devices";
if (normalized === "/login" || normalized.startsWith("/login/"))
return "auth";
@@ -122,7 +124,10 @@ const canonicalizeInitialPublicUrl = (route) => {
);
};
-if (initialMock === "trial" && initialRoute === "/trial") {
+if (initialMock === "emails") {
+ initialRoute = "/emails";
+ canonicalizeInitialPublicUrl(initialRoute);
+} else if (initialMock === "trial" && initialRoute === "/trial") {
initialRoute = "/home";
const normalizedUrl = new URL(window.location.href);
normalizedUrl.pathname = publicPathFromRoute(initialRoute);
@@ -134,12 +139,14 @@ if (initialMock === "trial" && initialRoute === "/trial") {
} else {
canonicalizeInitialPublicUrl(initialRoute);
}
-params.set("mock", initialMock);
-params.delete("path");
-params.delete("screen");
-params.delete("admin_section");
-params.set("path", initialRoute);
-frame.src = `${runtimeBase}/app/?${params.toString()}${window.location.hash || ""}`;
+if (initialMock !== "emails") {
+ params.set("mock", initialMock);
+ params.delete("path");
+ params.delete("screen");
+ params.delete("admin_section");
+ params.set("path", initialRoute);
+ frame.src = `${runtimeBase}/app/?${params.toString()}${window.location.hash || ""}`;
+}
const routeFromRuntimeUrl = (url) => {
if (url.origin !== window.location.origin) return "";
@@ -154,6 +161,7 @@ const routeFromRuntimeUrl = (url) => {
return runtimePath;
};
const routeForStateMock = (mock) => {
+ if (mock === "emails") return "/emails";
if (mock === "devices") return "/devices";
if (mock === "auth") return "/login";
return "/home";
@@ -169,8 +177,18 @@ const topbar = document.querySelector(".demo-topbar");
const toggle = document.querySelector(".demo-topbar__toggle");
const hide = document.querySelector(".demo-topbar__hide");
const stateSelect = document.querySelector(".demo-topbar__state-select");
+const emailPreviews = document.getElementById("email-previews");
+
+const setDemoMode = (mock) => {
+ const emailMode = mock === "emails";
+ frame.hidden = emailMode;
+ if (emailPreviews) emailPreviews.hidden = !emailMode;
+ if (emailMode) document.body.setAttribute("data-demo-mode", "emails");
+ else document.body.removeAttribute("data-demo-mode");
+};
const syncParentUrlFromFrame = () => {
+ if (frame.hidden) return;
try {
const frameUrl = new URL(frame.contentWindow.location.href);
const route = routeFromRuntimeUrl(frameUrl);
@@ -208,19 +226,21 @@ const setCollapsed = (collapsed) => {
toggle?.addEventListener("click", () => setCollapsed(false));
hide?.addEventListener("click", () => setCollapsed(true));
-if (stateSelect) stateSelect.value = normalizeStateMock(params.get("mock"));
+if (stateSelect) stateSelect.value = normalizeStateMock(params.get("mock") || initialMock);
+setDemoMode(initialMock);
stateSelect?.addEventListener("change", () => {
const mock = normalizeStateMock(stateSelect.value);
const nextParams = new URLSearchParams(window.location.search);
nextParams.delete("path");
nextParams.delete("screen");
nextParams.delete("admin_section");
- if (mock === defaultMock) nextParams.delete("mock");
+ if (mock === defaultMock || mock === "emails") nextParams.delete("mock");
else nextParams.set("mock", mock);
const query = nextParams.toString();
const stateRoute = routeForStateMock(mock);
const publicUrl = `${demoBase}${stateRoute}${query ? `?${query}` : ""}`;
window.history.replaceState(null, "", publicUrl);
- frame.src = runtimeSrc(stateRoute, nextParams);
+ setDemoMode(mock);
+ if (mock !== "emails") frame.src = runtimeSrc(stateRoute, nextParams);
});
diff --git a/docs-site/src/lib/demoRoutes.mjs b/docs-site/src/lib/demoRoutes.mjs
index 29b4969..aaed5fc 100644
--- a/docs-site/src/lib/demoRoutes.mjs
+++ b/docs-site/src/lib/demoRoutes.mjs
@@ -31,6 +31,7 @@ export const demoPublicRouteAliases = ["app"];
export const demoPublicRoutes = [
...demoPublicRouteAliases,
...demoUserRoutes,
+ "emails",
"admin",
...demoAdminRoutes.map((route) => `admin/${route}`),
];
diff --git a/docs-site/src/lib/emailPreviews.mjs b/docs-site/src/lib/emailPreviews.mjs
new file mode 100644
index 0000000..e18b388
--- /dev/null
+++ b/docs-site/src/lib/emailPreviews.mjs
@@ -0,0 +1,463 @@
+import { existsSync, readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+const localePathCandidates = [
+ resolve(process.cwd(), "..", "locales", "ru.json"),
+ resolve(process.cwd(), "locales", "ru.json"),
+];
+const localePath =
+ localePathCandidates.find((candidate) => existsSync(candidate)) ||
+ localePathCandidates[0];
+const messages = JSON.parse(readFileSync(localePath, "utf8"));
+
+const sample = {
+ amount: "390 RUB",
+ brand: "remnawave-minishop",
+ code: "483921",
+ dashboardUrl: "https://mini.example.com/app",
+ endDate: "21.06.2026, 18:00",
+ magicUrl: "https://mini.example.com/app/auth/magic/preview",
+ minutes: 10,
+ premiumTraffic: "25",
+ regularTraffic: "100",
+ ticketUrl: "https://mini.example.com/app/support/42",
+};
+
+const interpolate = (template, values = {}) =>
+ String(template || "").replace(/\{([a-zA-Z0-9_]+)\}/g, (match, name) =>
+ Object.prototype.hasOwnProperty.call(values, name) ? values[name] : match,
+ );
+
+const t = (key, values = {}) => interpolate(messages[key] || key, values);
+
+const escapeHtml = (value) =>
+ String(value || "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+
+const textWithBreaks = (value) => escapeHtml(value).replace(/\n/g, "
");
+
+const stripTags = (value) =>
+ String(value || "")
+ .replace(/
/gi, "\n")
+ .replace(/<[^>]*>/g, "");
+
+const renderRows = (rows = []) => {
+ if (!rows.length) return "";
+ return `
+ ${rows
+ .map(
+ ([label, value]) => `
+ | ${escapeHtml(label)} |
+ ${escapeHtml(value)} |
+
`,
+ )
+ .join("")}
+
`;
+};
+
+const renderEmailHtml = ({
+ subject,
+ heading = subject,
+ intro,
+ rows,
+ code,
+ message,
+ note,
+ ctaLabel,
+ ctaUrl,
+}) => `
+
${escapeHtml(sample.brand)}
+
+
${escapeHtml(subject)}
+
${escapeHtml(heading)}
+ ${intro ? `
${textWithBreaks(intro)}
` : ""}
+ ${
+ code
+ ? `
${escapeHtml(code)}
`
+ : ""
+ }
+ ${renderRows(rows)}
+ ${
+ message
+ ? `
${textWithBreaks(message)}
`
+ : ""
+ }
+ ${
+ ctaLabel
+ ? `
${escapeHtml(
+ ctaLabel,
+ )}`
+ : ""
+ }
+ ${note ? `
${textWithBreaks(note)}
` : ""}
+
+
+
`;
+
+const preview = (item) => ({
+ ...item,
+ html: renderEmailHtml(item),
+});
+
+const paymentRows = (periodLabel, periodValue, provider = "YooKassa") => [
+ [periodLabel, periodValue],
+ [t("email_payment_success_row_amount"), sample.amount],
+ [t("email_payment_success_row_end_date"), sample.endDate],
+ [t("email_payment_success_row_method"), provider],
+];
+
+const paymentPreview = ({
+ id,
+ title,
+ introKey,
+ periodLabel,
+ periodValue,
+ textKey,
+ values = {},
+}) =>
+ preview({
+ id,
+ category: "Платежи",
+ title,
+ subject: t("email_payment_success_subject"),
+ heading: t("email_payment_success_heading"),
+ intro: t(introKey, values),
+ rows: paymentRows(periodLabel, periodValue),
+ message: t(textKey, {
+ amount: sample.amount,
+ end_date: sample.endDate,
+ ...values,
+ }),
+ note: t("email_payment_success_footer_note"),
+ ctaLabel: t("email_payment_success_cta"),
+ });
+
+const notificationPreview = ({
+ id,
+ title,
+ subjectKey,
+ message,
+ ctaKey = "email_user_notification_cta",
+}) =>
+ preview({
+ id,
+ category: "Уведомления",
+ title,
+ subject: t(subjectKey),
+ heading: t(subjectKey),
+ intro: t("email_user_notification_intro"),
+ message,
+ ctaLabel: t(ctaKey),
+ });
+
+const expiringPreview = ({ id, title, suffix, days }) =>
+ preview({
+ id,
+ category: "Подписка",
+ title,
+ subject: t(`email_subscription_expiring_subject_${suffix}`, { days }),
+ heading: t(`email_subscription_expiring_heading_${suffix}`, { days }),
+ intro: t(`email_subscription_expiring_intro_${suffix}`, { days }),
+ rows: [
+ [t("email_subscription_expiring_row_days_left"), String(days)],
+ [t("email_subscription_expiring_row_end_date"), sample.endDate],
+ ],
+ message: t("email_subscription_expiring_text", {
+ heading: t(`email_subscription_expiring_heading_${suffix}`, { days }),
+ end_date: sample.endDate,
+ }),
+ note: t("email_subscription_expiring_note"),
+ ctaLabel: t("email_subscription_expiring_cta"),
+ });
+
+const lifecycleSubject = (key, values = {}) => t(key, values);
+
+const lifecyclePreview = ({
+ id,
+ title,
+ subject,
+ introKey = "email_subscription_lifecycle_intro_direct",
+ message,
+}) =>
+ preview({
+ id,
+ category: "Подписка",
+ title,
+ subject,
+ heading: subject,
+ intro: t(introKey),
+ rows: [[t("email_subscription_lifecycle_row_end_date"), sample.endDate]],
+ message,
+ ctaLabel: t("email_subscription_lifecycle_cta"),
+ });
+
+const supportRows = (includeUser = false) => [
+ [t("email_support_row_ticket"), "#42"],
+ ...(includeUser ? [[t("email_support_row_user"), "alex@example.com"]] : []),
+ [t("email_support_row_subject"), "Не работает подключение"],
+ [t("email_support_row_tariff"), "Premium"],
+ [t("email_support_row_remaining"), "3 д. 4 ч."],
+];
+
+export const emailPreviews = [
+ preview({
+ id: "login-code",
+ category: "Доступ",
+ title: "Код для входа",
+ subject: t("email_login_code_subject", { code: sample.code }),
+ heading: t("email_login_code_heading"),
+ intro: t("email_login_code_intro"),
+ code: sample.code,
+ message: [
+ stripTags(t("email_login_code_expiry_html", { minutes: sample.minutes })),
+ t("email_login_code_security"),
+ t("email_login_code_text_magic", { url: sample.magicUrl }),
+ ].join("\n\n"),
+ ctaLabel: t("email_login_code_magic_cta"),
+ ctaUrl: sample.magicUrl,
+ }),
+ preview({
+ id: "set-password-code",
+ category: "Доступ",
+ title: "Код для создания пароля",
+ subject: t("email_set_password_code_subject", { code: sample.code }),
+ heading: t("email_set_password_code_heading"),
+ intro: t("email_set_password_code_intro"),
+ code: sample.code,
+ message: [
+ stripTags(t("email_set_password_code_expiry_html", { minutes: sample.minutes })),
+ t("email_set_password_code_security"),
+ ].join("\n\n"),
+ }),
+ preview({
+ id: "account-merged",
+ category: "Аккаунт",
+ title: "Аккаунты объединены",
+ subject: t("email_account_merged_subject"),
+ heading: t("email_account_merged_heading"),
+ intro: t("email_account_merged_intro"),
+ rows: [
+ [t("email_account_merged_row_kept"), "#100200300"],
+ [t("email_account_merged_row_removed"), "#-42"],
+ [t("email_account_merged_row_end_date"), sample.endDate],
+ ],
+ message: t("email_account_merged_text", {
+ primary: "#100200300",
+ removed: "#-42",
+ end_date: sample.endDate,
+ }),
+ note: t("email_account_merged_note"),
+ }),
+ paymentPreview({
+ id: "payment-subscription",
+ title: "Оплата подписки",
+ introKey: "email_payment_success_intro_subscription",
+ periodLabel: t("email_payment_success_row_period"),
+ periodValue: t("email_payment_success_period_value", { months: 1 }),
+ textKey: "email_payment_success_text_subscription",
+ values: { months: 1 },
+ }),
+ paymentPreview({
+ id: "payment-traffic",
+ title: "Покупка трафика",
+ introKey: "email_payment_success_intro_traffic",
+ periodLabel: t("email_payment_success_row_traffic"),
+ periodValue: t("email_payment_success_traffic_value", {
+ traffic_gb: sample.regularTraffic,
+ }),
+ textKey: "email_payment_success_text_traffic",
+ values: { traffic_gb: sample.regularTraffic },
+ }),
+ paymentPreview({
+ id: "payment-premium-traffic",
+ title: "Покупка premium-трафика",
+ introKey: "email_payment_success_intro_premium_topup",
+ periodLabel: t("email_payment_success_row_traffic"),
+ periodValue: t("email_payment_success_traffic_value", {
+ traffic_gb: sample.premiumTraffic,
+ }),
+ textKey: "email_payment_success_text_traffic",
+ values: { traffic_gb: sample.premiumTraffic },
+ }),
+ paymentPreview({
+ id: "payment-hwid",
+ title: "Покупка HWID-устройств",
+ introKey: "email_payment_success_intro_hwid",
+ periodLabel: t("email_payment_success_row_hwid"),
+ periodValue: t("email_payment_success_hwid_value", { count: 2 }),
+ textKey: "email_payment_success_text_hwid",
+ values: { count: 2 },
+ }),
+ paymentPreview({
+ id: "payment-tariff-upgrade",
+ title: "Платное повышение тарифа",
+ introKey: "email_payment_success_intro_tariff_upgrade",
+ periodLabel: t("email_payment_success_row_operation"),
+ periodValue: t("email_payment_success_tariff_upgrade_value"),
+ textKey: "email_payment_success_text_tariff_upgrade",
+ }),
+ notificationPreview({
+ id: "payment-failed",
+ title: "Неуспешная оплата",
+ subjectKey: "email_payment_failed_subject",
+ message: "Платёж не был завершён. Можно попробовать ещё раз из личного кабинета.",
+ }),
+ notificationPreview({
+ id: "payment-method-bound",
+ title: "Способ оплаты привязан",
+ subjectKey: "email_payment_method_bound_subject",
+ message: "Автопродление подключено, следующий платёж пройдёт автоматически.",
+ }),
+ notificationPreview({
+ id: "referral-bonus",
+ title: "Реферальный бонус",
+ subjectKey: "email_referral_bonus_subject",
+ message: "Друг активировал подписку, и бонусные дни уже добавлены к вашему аккаунту.",
+ }),
+ notificationPreview({
+ id: "trial-traffic-depleted",
+ title: "Трафик пробного периода закончился",
+ subjectKey: "email_trial_traffic_depleted_subject",
+ message: "Пробный трафик израсходован. Оформите подписку, чтобы продолжить пользоваться сервисом.",
+ }),
+ notificationPreview({
+ id: "regular-traffic-almost",
+ title: "Обычный трафик почти закончился",
+ subjectKey: "email_traffic_warning_regular_almost_subject",
+ ctaKey: "email_traffic_warning_regular_cta",
+ message: "Использовано больше 85% трафика тарифа. Можно докупить пакет заранее.",
+ }),
+ notificationPreview({
+ id: "regular-traffic-depleted",
+ title: "Обычный трафик закончился",
+ subjectKey: "email_traffic_warning_regular_depleted_subject",
+ ctaKey: "email_traffic_warning_regular_cta",
+ message: "Трафик тарифа израсходован. Докупите пакет, чтобы восстановить доступ.",
+ }),
+ notificationPreview({
+ id: "premium-traffic-almost",
+ title: "Premium-трафик почти закончился",
+ subjectKey: "email_traffic_warning_premium_almost_subject",
+ ctaKey: "email_traffic_warning_premium_cta",
+ message: "Premium-трафика осталось мало. Можно докупить пакет до полного расхода.",
+ }),
+ notificationPreview({
+ id: "premium-traffic-depleted",
+ title: "Premium-трафик закончился",
+ subjectKey: "email_traffic_warning_premium_depleted_subject",
+ ctaKey: "email_traffic_warning_premium_cta",
+ message: "Premium-трафик израсходован. Докупите пакет, чтобы продолжить использовать premium-маршруты.",
+ }),
+ expiringPreview({
+ id: "subscription-expiring-today",
+ title: "Подписка заканчивается сегодня",
+ suffix: "today",
+ days: 0,
+ }),
+ expiringPreview({
+ id: "subscription-expiring-tomorrow",
+ title: "Подписка заканчивается завтра",
+ suffix: "tomorrow",
+ days: 1,
+ }),
+ expiringPreview({
+ id: "subscription-expiring-days",
+ title: "Подписка скоро закончится",
+ suffix: "days",
+ days: 3,
+ }),
+ lifecyclePreview({
+ id: "lifecycle-before-days",
+ title: "Lifecycle: осталось несколько дней",
+ subject: lifecycleSubject("email_subscription_lifecycle_subject_before_days", {
+ days: 3,
+ }),
+ message: "Подписка скоро закончится. Продлите её заранее, чтобы доступ не прерывался.",
+ }),
+ lifecyclePreview({
+ id: "lifecycle-before-hours",
+ title: "Lifecycle: осталось несколько часов",
+ subject: lifecycleSubject("email_subscription_lifecycle_subject_before_hours", {
+ hours: 6,
+ }),
+ message: "До окончания подписки осталось несколько часов.",
+ }),
+ lifecyclePreview({
+ id: "lifecycle-expired",
+ title: "Lifecycle: подписка закончилась",
+ subject: lifecycleSubject("email_subscription_lifecycle_subject_expired"),
+ message: "Подписка закончилась. Продлите доступ в личном кабинете.",
+ }),
+ lifecyclePreview({
+ id: "lifecycle-expired-after",
+ title: "Lifecycle: подписка закончилась вчера",
+ subject: lifecycleSubject("email_subscription_lifecycle_subject_expired_after"),
+ message: "Вчера подписка была отключена. Вы можете восстановить доступ продлением.",
+ }),
+ lifecyclePreview({
+ id: "lifecycle-autorenew",
+ title: "Lifecycle: автопродление завтра",
+ subject: lifecycleSubject("email_subscription_lifecycle_subject_autorenew"),
+ message: "Завтра будет выполнено автопродление подписки.",
+ }),
+ lifecyclePreview({
+ id: "lifecycle-mirrored",
+ title: "Lifecycle: копия Telegram-уведомления",
+ subject: lifecycleSubject("email_subscription_lifecycle_subject_before_days", {
+ days: 2,
+ }),
+ introKey: "email_subscription_lifecycle_intro_mirrored",
+ message: "Это письмо дублирует важное уведомление, отправленное в Telegram.",
+ }),
+ preview({
+ id: "support-new-ticket-admin",
+ category: "Поддержка",
+ title: "Новый тикет для администратора",
+ subject: t("email_support_new_ticket_admin_subject", { ticket_id: 42 }),
+ heading: t("email_support_new_ticket_admin_heading", { ticket_id: 42 }),
+ intro: t("email_support_new_ticket_admin_intro"),
+ rows: supportRows(true),
+ message: "Пользователь не может подключиться после продления.",
+ ctaLabel: t("email_support_cta_open_ticket"),
+ ctaUrl: "https://mini.example.com/app/admin/support/42",
+ }),
+ preview({
+ id: "support-user-reply-admin",
+ category: "Поддержка",
+ title: "Ответ пользователя для администратора",
+ subject: t("email_support_user_reply_admin_subject", { ticket_id: 42 }),
+ heading: t("email_support_user_reply_admin_heading", { ticket_id: 42 }),
+ intro: t("email_support_user_reply_admin_intro"),
+ rows: supportRows(true),
+ message: "Проблема повторилась на телефоне и ноутбуке.",
+ ctaLabel: t("email_support_cta_open_ticket"),
+ ctaUrl: "https://mini.example.com/app/admin/support/42",
+ }),
+ preview({
+ id: "support-admin-reply-user",
+ category: "Поддержка",
+ title: "Ответ поддержки пользователю",
+ subject: t("email_support_admin_reply_user_subject", { ticket_id: 42 }),
+ heading: t("email_support_admin_reply_user_heading", { ticket_id: 42 }),
+ intro: t("email_support_admin_reply_user_intro"),
+ rows: supportRows(false),
+ message: "Мы обновили конфигурацию. Попробуйте подключиться ещё раз.",
+ ctaLabel: t("email_support_cta_open_mini_app"),
+ ctaUrl: sample.ticketUrl,
+ }),
+ preview({
+ id: "support-ticket-closed-user",
+ category: "Поддержка",
+ title: "Тикет закрыт",
+ subject: t("email_support_ticket_closed_user_subject", { ticket_id: 42 }),
+ heading: t("email_support_ticket_closed_user_heading", { ticket_id: 42 }),
+ intro: t("email_support_ticket_closed_user_intro"),
+ rows: supportRows(false),
+ message: t("email_support_ticket_closed_user_body"),
+ ctaLabel: t("email_support_cta_open_mini_app"),
+ ctaUrl: sample.ticketUrl,
+ }),
+];
diff --git a/docs-site/src/pages/demo.astro b/docs-site/src/pages/demo.astro
index 39a3d5b..9028d85 100644
--- a/docs-site/src/pages/demo.astro
+++ b/docs-site/src/pages/demo.astro
@@ -1,4 +1,6 @@
---
+import { emailPreviews } from '../lib/emailPreviews.mjs';
+
const defaultDemoSrc = '/demo/runtime/app/?path=/home&mock=tariffs';
const docsHref = '/getting-started/demo/';
---
@@ -44,6 +46,10 @@ const docsHref = '/getting-started/demo/';
background: #05080f;
}
+ body[data-demo-mode='emails'] {
+ overflow: auto;
+ }
+
.demo-topbar {
display: flex;
gap: 1rem;
@@ -151,6 +157,275 @@ const docsHref = '/getting-started/demo/';
background: #05080f;
}
+ .demo-frame[hidden],
+ .email-previews[hidden] {
+ display: none;
+ }
+
+ .email-previews {
+ min-height: 0;
+ overflow: auto;
+ background:
+ linear-gradient(180deg, rgb(15 23 42 / 32%), rgb(5 8 15 / 0) 14rem),
+ #05080f;
+ }
+
+ .email-previews__inner {
+ width: min(72rem, calc(100% - 2rem));
+ margin: 0 auto;
+ padding: 2rem 0 4rem;
+ }
+
+ .email-previews__header {
+ display: grid;
+ gap: 0.45rem;
+ max-width: 44rem;
+ margin-bottom: 1.25rem;
+ }
+
+ .email-previews__eyebrow {
+ color: #00fe7a;
+ font-size: 0.78rem;
+ font-weight: 800;
+ line-height: 1;
+ text-transform: uppercase;
+ }
+
+ .email-previews__header h1 {
+ margin: 0;
+ color: #ffffff;
+ font-size: 1.65rem;
+ line-height: 1.15;
+ }
+
+ .email-previews__header p {
+ margin: 0;
+ color: #a7b1c2;
+ font-size: 0.95rem;
+ line-height: 1.55;
+ }
+
+ .email-previews__index {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem;
+ margin: 0 0 1.4rem;
+ }
+
+ .email-previews__index a {
+ border: 1px solid rgb(148 163 184 / 24%);
+ border-radius: 7px;
+ padding: 0.42rem 0.6rem;
+ background: rgb(15 23 42 / 62%);
+ color: #dbeafe;
+ font-size: 0.78rem;
+ font-weight: 650;
+ line-height: 1;
+ text-decoration: none;
+ }
+
+ .email-previews__index a:hover {
+ border-color: rgb(0 254 122 / 54%);
+ color: #00fe7a;
+ }
+
+ .email-preview {
+ border-top: 1px solid rgb(148 163 184 / 18%);
+ }
+
+ .email-preview:last-child {
+ border-bottom: 1px solid rgb(148 163 184 / 18%);
+ }
+
+ .email-preview summary {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 1rem;
+ align-items: center;
+ padding: 1rem 0;
+ cursor: pointer;
+ list-style: none;
+ }
+
+ .email-preview summary::-webkit-details-marker {
+ display: none;
+ }
+
+ .email-preview__title {
+ min-width: 0;
+ }
+
+ .email-preview__title strong {
+ display: block;
+ color: #f8fafc;
+ font-size: 0.98rem;
+ line-height: 1.3;
+ }
+
+ .email-preview__title span {
+ display: block;
+ margin-top: 0.22rem;
+ color: #94a3b8;
+ font-size: 0.78rem;
+ line-height: 1.35;
+ }
+
+ .email-preview__meta {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid rgb(14 165 233 / 34%);
+ border-radius: 999px;
+ padding: 0.28rem 0.55rem;
+ color: #bae6fd;
+ font-size: 0.72rem;
+ font-weight: 750;
+ line-height: 1;
+ white-space: nowrap;
+ }
+
+ .email-preview__body {
+ padding: 0.1rem 0 1.6rem;
+ }
+
+ .mail-card {
+ width: min(32rem, 100%);
+ margin: 0 auto;
+ padding: 1.35rem 1rem;
+ border-radius: 8px;
+ background: #05070a;
+ color: #e6e9ef;
+ font-family:
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
+ sans-serif;
+ }
+
+ .mail-card__brand {
+ margin-bottom: 1rem;
+ color: #00fe7a;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 1.25rem;
+ font-weight: 850;
+ line-height: 1.1;
+ text-align: center;
+ }
+
+ .mail-card__panel {
+ border: 1px solid #1a1f27;
+ border-radius: 8px;
+ padding: 1.4rem;
+ background: #0e1116;
+ }
+
+ .mail-card__subject {
+ margin: 0 0 0.7rem;
+ color: #00fe7a;
+ font-size: 0.78rem;
+ font-weight: 750;
+ line-height: 1.35;
+ }
+
+ .mail-card h2 {
+ margin: 0 0 0.7rem;
+ color: #ffffff;
+ font-size: 1.16rem;
+ line-height: 1.25;
+ }
+
+ .mail-card__intro,
+ .mail-card__note,
+ .mail-card__footer {
+ color: #9aa3b2;
+ font-size: 0.86rem;
+ line-height: 1.55;
+ }
+
+ .mail-card__intro {
+ margin: 0 0 1rem;
+ }
+
+ .mail-card__note {
+ margin: 0.9rem 0 0;
+ color: #707987;
+ font-size: 0.78rem;
+ }
+
+ .mail-card__footer {
+ margin: 0.9rem 0 0;
+ color: #5d6573;
+ font-size: 0.72rem;
+ text-align: center;
+ }
+
+ .mail-card__code {
+ margin: 0 0 1rem;
+ border: 1px solid #1a1f27;
+ border-radius: 8px;
+ padding: 1rem;
+ background: #05070a;
+ color: #00fe7a;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 2rem;
+ font-weight: 800;
+ line-height: 1;
+ text-align: center;
+ }
+
+ .mail-card__rows {
+ width: 100%;
+ margin: 0 0 1rem;
+ border: 1px solid #1a1f27;
+ border-radius: 8px;
+ padding: 0.3rem 0.9rem;
+ background: #05070a;
+ }
+
+ .mail-card__rows td {
+ border-bottom: 1px solid #1a1f27;
+ padding: 0.64rem 0;
+ color: #5d6573;
+ font-size: 0.72rem;
+ font-weight: 750;
+ line-height: 1.2;
+ text-transform: uppercase;
+ }
+
+ .mail-card__rows tr:last-child td {
+ border-bottom: 0;
+ }
+
+ .mail-card__rows td:last-child {
+ color: #e6e9ef;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.82rem;
+ text-align: right;
+ text-transform: none;
+ }
+
+ .mail-card__message {
+ margin: 0 0 1rem;
+ border: 1px solid #1a1f27;
+ border-radius: 8px;
+ padding: 0.85rem 0.95rem;
+ background: #05070a;
+ color: #e6e9ef;
+ font-size: 0.9rem;
+ line-height: 1.55;
+ }
+
+ .mail-card__cta {
+ display: block;
+ border-radius: 8px;
+ padding: 0.9rem 1rem;
+ background: #00fe7a;
+ color: #05070a;
+ font-size: 0.92rem;
+ font-weight: 800;
+ line-height: 1;
+ text-align: center;
+ text-decoration: none;
+ }
+
@media (max-width: 42rem) {
body {
display: block;
@@ -269,6 +544,32 @@ const docsHref = '/getting-started/demo/';
.demo-frame {
height: 100dvh;
}
+
+ .email-previews {
+ min-height: 100dvh;
+ }
+
+ .email-previews__inner {
+ width: min(calc(100% - 1rem), 72rem);
+ padding-top: 4rem;
+ }
+
+ .email-preview summary {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 0.55rem;
+ }
+
+ .email-preview__meta {
+ width: fit-content;
+ }
+
+ .mail-card__panel {
+ padding: 1rem;
+ }
+
+ .mail-card__code {
+ font-size: 1.65rem;
+ }
}
@@ -296,6 +597,7 @@ const docsHref = '/getting-started/demo/';
+
@@ -313,6 +615,46 @@ const docsHref = '/getting-started/demo/';
src={defaultDemoSrc}
loading="eager"
>
+
+
+
+
+
+ {
+ emailPreviews.map((preview, index) => (
+
+
+
+ {preview.title}
+ {preview.subject}
+
+ {preview.category}
+
+
+
+ ))
+ }
+
+
+