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]) => ` + + + `, + ) + .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" > +