Files
termi-blog/frontend/src/components/SubscriptionPopup.astro
limitcool 381dc9b854
Some checks failed
docker-images / resolve-build-targets (push) Successful in 5s
docker-images / build-and-push (admin) (push) Successful in 30s
docker-images / submit-indexnow (push) Has been cancelled
docker-images / build-and-push (frontend) (push) Has been cancelled
docker-images / build-and-push (backend) (push) Has been cancelled
Fix web push delivery handling and worker console
2026-04-04 04:15:20 +08:00

1907 lines
60 KiB
Plaintext

---
import {
resolvePublicApiBaseUrl,
resolvePublicCommentTurnstileSiteKey,
resolvePublicWebPushVapidPublicKey,
} from '../lib/api/client';
import type { SiteSettings } from '../lib/types';
interface Props {
requestUrl?: string | URL;
siteSettings: SiteSettings;
}
const { requestUrl, siteSettings } = Astro.props as Props;
const subscribeApiUrl = `${resolvePublicApiBaseUrl(requestUrl)}/subscriptions`;
const combinedSubscribeApiUrl = `${resolvePublicApiBaseUrl(requestUrl)}/subscriptions/combined`;
const browserPushApiUrl = `${resolvePublicApiBaseUrl(requestUrl)}/subscriptions/browser-push`;
const captchaApiUrl = `${resolvePublicApiBaseUrl(requestUrl)}/comments/captcha`;
const popupSettings = siteSettings.subscriptions;
const verificationMode = popupSettings.verificationMode;
const turnstileSiteKey = verificationMode === 'turnstile'
? popupSettings.turnstileSiteKey || resolvePublicCommentTurnstileSiteKey()
: '';
const webPushPublicKey = popupSettings.webPushVapidPublicKey || resolvePublicWebPushVapidPublicKey() || '';
const webPushAvailable = Boolean(webPushPublicKey);
---
{popupSettings.popupEnabled && (
<div
class="subscription-popup-root"
data-subscription-popup-root
data-api-url={subscribeApiUrl}
data-combined-api-url={combinedSubscribeApiUrl}
data-browser-push-api-url={browserPushApiUrl}
data-captcha-url={captchaApiUrl}
data-delay-ms={String(Math.max(popupSettings.popupDelaySeconds, 3) * 1000)}
data-verification-mode={verificationMode}
data-turnstile-site-key={turnstileSiteKey || undefined}
data-web-push-public-key={webPushPublicKey || undefined}
hidden
>
<section
class="subscription-popup-panel terminal-panel"
data-subscription-popup-panel
role="dialog"
aria-labelledby="subscription-popup-title"
aria-describedby="subscription-popup-description"
>
<button
type="button"
class="subscription-popup-close"
data-subscription-popup-close
aria-label="关闭订阅提示"
>
<i class="fas fa-xmark"></i>
</button>
<div class="subscription-popup-main">
<div class="subscription-popup-copy">
<div class="subscription-popup-copy-surface">
<div class="subscription-popup-copy-head">
<div class="subscription-popup-copy-mark">
<span class="subscription-popup-icon" aria-hidden="true">
<i class="fas fa-paper-plane"></i>
</span>
<div class="subscription-popup-copy-body">
<p class="terminal-kicker">reader updates</p>
<h3 id="subscription-popup-title">{popupSettings.popupTitle}</h3>
<p id="subscription-popup-description">{popupSettings.popupDescription}</p>
</div>
</div>
<div class="subscription-popup-badges" aria-hidden="true">
{webPushAvailable && <span class="subscription-popup-badge">即时提醒</span>}
<span class="subscription-popup-badge">新文章</span>
<span class="subscription-popup-badge">低频提醒</span>
</div>
</div>
<ul class="subscription-popup-points" aria-hidden="true">
<li>
<span class="subscription-popup-point-icon"><i class="fas fa-check"></i></span>
<span>只在有新内容或值得提醒的时候通知你,不做高频打扰</span>
</li>
<li>
<span class="subscription-popup-point-icon"><i class="fas fa-check"></i></span>
<span>支持即时提醒和邮件备份两种方式</span>
</li>
<li>
<span class="subscription-popup-point-icon"><i class="fas fa-check"></i></span>
<span>可随时暂停、管理或退订</span>
</li>
</ul>
<div class="subscription-popup-preview" aria-hidden="true">
<div class="subscription-popup-preview-window">
<div class="subscription-popup-preview-topbar">
<span class="subscription-popup-preview-dot"></span>
<span class="subscription-popup-preview-dot"></span>
<span class="subscription-popup-preview-dot"></span>
</div>
<div class="subscription-popup-preview-card">
<span class="subscription-popup-preview-kicker">instant update</span>
<strong>有新内容时第一时间提醒你</strong>
<p>新的文章、专题整理或精选内容发布后,你会更快收到通知。</p>
<div class="subscription-popup-preview-tags">
<span>文章更新</span>
<span>汇总简报</span>
<span>可退订</span>
</div>
</div>
</div>
</div>
</div>
<div class="subscription-popup-meta">
{webPushAvailable && (
<span class="terminal-stat-pill">
<i class="fas fa-bell text-[var(--primary)]"></i>
开启后生效
</span>
)}
<span class="terminal-stat-pill">
<i class="fas fa-newspaper text-[var(--primary)]"></i>
新文章 / 汇总简报
</span>
<span class="terminal-stat-pill">
<i class="fas fa-envelope-open-text text-[var(--primary)]"></i>
邮箱确认后生效
</span>
<span class="terminal-stat-pill">
<i class="fas fa-user-shield text-[var(--primary)]"></i>
随时可退订
</span>
</div>
</div>
<div class="subscription-popup-side">
<form class="subscription-popup-form subscription-popup-channel-card" data-subscription-popup-form>
<div class="subscription-popup-side-intro">
<div class="subscription-popup-side-intro-copy">
<p class="subscription-popup-channel-kicker">optional email</p>
<h4>浏览器提醒会默认开启</h4>
<p>如果你还想额外留一个邮箱备份,可以再补开邮件订阅。</p>
</div>
<div class="subscription-popup-channel-selector">
<button
type="button"
class="subscription-popup-channel-toggle subscription-popup-channel-toggle--email"
data-subscription-popup-channel-toggle="email"
aria-pressed="false"
>
<span class="subscription-popup-channel-toggle-icon" aria-hidden="true">
<i class="fas fa-envelope-open-text"></i>
</span>
<span class="subscription-popup-channel-toggle-copy">
<span class="subscription-popup-channel-toggle-meta" aria-hidden="true">
<span class="subscription-popup-channel-toggle-tag">可选</span>
<span class="subscription-popup-channel-toggle-tag subscription-popup-channel-toggle-tag--email">
Email
</span>
</span>
<span class="subscription-popup-channel-toggle-title" data-subscription-popup-channel-toggle-label>
添加邮件订阅
</span>
<span
class="subscription-popup-channel-toggle-description"
data-subscription-popup-channel-toggle-description
>
填写邮箱后,更新也会发到你的收件箱
</span>
</span>
<span class="subscription-popup-channel-toggle-affordance" aria-hidden="true">
<span
class="subscription-popup-channel-toggle-affordance-text"
data-subscription-popup-channel-toggle-affordance
>
去填写
</span>
<i class="fas fa-chevron-right"></i>
</span>
</button>
</div>
</div>
<p class="subscription-popup-channel-helper">
浏览器提醒依然是必选主流程;邮件只作为额外备份。
</p>
{webPushAvailable ? (
<div
class="subscription-popup-channel-card terminal-panel-muted"
data-subscription-popup-channel-card="browser"
>
<div class="subscription-popup-channel-head">
<span class="subscription-popup-channel-icon" aria-hidden="true">
<i class="fas fa-bell"></i>
</span>
<div>
<p class="subscription-popup-channel-kicker">instant updates</p>
<h4>开启即时提醒</h4>
</div>
</div>
<p class="subscription-popup-channel-copy">
开启后,有新内容时你会更快收到提醒。
</p>
<p class="subscription-popup-channel-note" data-subscription-popup-browser-note>
默认优先推荐这个方式;只选它时点确定即可,不需要填写额外信息。
</p>
</div>
) : (
<div class="subscription-popup-channel-card terminal-panel-muted">
<div class="subscription-popup-channel-head">
<span class="subscription-popup-channel-icon" aria-hidden="true">
<i class="fas fa-bell-slash"></i>
</span>
<div>
<p class="subscription-popup-channel-kicker">instant updates</p>
<h4>即时提醒暂不可用</h4>
</div>
</div>
<p class="subscription-popup-channel-copy">
当前暂时还不能开启即时提醒,请稍后再试。
</p>
<p class="subscription-popup-channel-note" data-subscription-popup-browser-note>
订阅时即时提醒是主方式,这里不会再退回成邮件主流程。
</p>
</div>
)}
<div class="subscription-popup-channel-card" data-subscription-popup-channel-card="email" hidden>
<div class="subscription-popup-channel-head">
<span class="subscription-popup-channel-icon subscription-popup-channel-icon--mail" aria-hidden="true">
<i class="fas fa-envelope-open-text"></i>
</span>
<div>
<p class="subscription-popup-channel-kicker">backup mail</p>
<h4>邮箱备份</h4>
</div>
</div>
<p class="subscription-popup-channel-copy">
勾选后作为备用通知渠道;万一错过提醒,邮箱里也会留一份。
</p>
<label class="subscription-popup-field">
<span class="subscription-popup-field-label">称呼</span>
<input
type="text"
name="displayName"
placeholder="怎么称呼你(可选)"
autocomplete="name"
/>
</label>
<label class="subscription-popup-field">
<span class="subscription-popup-field-label">邮箱地址</span>
<input
type="email"
name="email"
placeholder="name@example.com"
autocomplete="email"
data-subscription-popup-email
/>
</label>
</div>
{verificationMode !== 'off' && (
<div class="subscription-popup-human-check" data-subscription-popup-human-check hidden>
<div class="flex items-center justify-between gap-3">
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-[var(--text-tertiary)]">
人机验证
</p>
{verificationMode === 'turnstile' ? (
<span class="text-xs text-[var(--text-tertiary)]">安全校验</span>
) : (
<button
type="button"
class="terminal-action-button px-3 py-2 text-xs"
data-subscription-popup-refresh-captcha
>
<i class="fas fa-rotate-right"></i>
<span>刷新</span>
</button>
)}
</div>
{verificationMode === 'turnstile' ? (
<>
<div class="mt-3" data-subscription-popup-turnstile></div>
<input type="hidden" name="turnstileToken" />
</>
) : (
<>
<p
class="mt-3 text-sm text-[var(--text-secondary)]"
data-subscription-popup-captcha-question
>
加载中...
</p>
<input type="hidden" name="captchaToken" />
<input
type="text"
name="captchaAnswer"
inputmode="numeric"
placeholder="请输入上方答案"
class="mt-3 terminal-form-input"
/>
</>
)}
</div>
)}
<div class="subscription-popup-actions">
<button
type="submit"
class="subscription-popup-channel-toggle subscription-popup-channel-toggle--primary subscription-popup-primary-button"
data-subscription-popup-submit
>
<span class="subscription-popup-channel-toggle-icon" aria-hidden="true">
<i class="fas fa-bell"></i>
</span>
<span class="subscription-popup-channel-toggle-copy">
<span class="subscription-popup-channel-toggle-title" data-subscription-popup-submit-label>
开启即时提醒
</span>
<span class="subscription-popup-channel-toggle-description">
默认点一下就能完成浏览器订阅
</span>
</span>
</button>
<button
type="button"
class="subscription-popup-channel-toggle subscription-popup-channel-toggle--ghost"
data-subscription-popup-dismiss
>
<span class="subscription-popup-channel-toggle-icon" aria-hidden="true">
<i class="fas fa-clock"></i>
</span>
<span class="subscription-popup-channel-toggle-copy">
<span class="subscription-popup-channel-toggle-title">稍后提醒</span>
<span class="subscription-popup-channel-toggle-description">
先继续浏览,之后再决定
</span>
</span>
</button>
</div>
</form>
</div>
</div>
<p class="subscription-popup-status" data-subscription-popup-status aria-live="polite">
只在有新文章或汇总简报时发送提醒,不会把它做成高频打扰。
</p>
</section>
</div>
)}
<script>
import { mountTurnstile, type MountedTurnstile } from '../lib/utils/turnstile';
import {
ensureBrowserPushSubscription,
getBrowserPushSubscriptionState,
supportsBrowserPush,
} from '../lib/utils/web-push';
(() => {
const DISMISS_KEY = 'termi:subscription-popup:dismiss-until';
const SUBSCRIBED_KEY = 'termi:subscription-popup:subscribed-at';
const DISMISS_TTL_MS = 7 * 24 * 60 * 60 * 1000;
document.querySelectorAll('[data-subscription-popup-root]').forEach((root) => {
if (!(root instanceof HTMLElement)) {
return;
}
const form = root.querySelector('[data-subscription-popup-form]');
const status = root.querySelector('[data-subscription-popup-status]');
const emailInput = root.querySelector('[data-subscription-popup-email]');
const dismissButton = root.querySelector('[data-subscription-popup-dismiss]');
const apiUrl = root.getAttribute('data-api-url');
const combinedApiUrl = root.getAttribute('data-combined-api-url') || apiUrl;
const captchaApiUrl = root.getAttribute('data-captcha-url') || '/api/comments/captcha';
const browserPushPublicKey = root.getAttribute('data-web-push-public-key') || '';
const submitButton = root.querySelector(
'[data-subscription-popup-submit]',
) as HTMLButtonElement | null;
const submitButtonLabel = submitButton?.querySelector(
'[data-subscription-popup-submit-label]',
) as HTMLElement | null;
const emailToggleButton = root.querySelector(
'[data-subscription-popup-channel-toggle="email"]',
) as HTMLButtonElement | null;
const emailToggleLabel = emailToggleButton?.querySelector(
'[data-subscription-popup-channel-toggle-label]',
) as HTMLElement | null;
const emailToggleDescription = emailToggleButton?.querySelector(
'[data-subscription-popup-channel-toggle-description]',
) as HTMLElement | null;
const emailToggleAffordance = emailToggleButton?.querySelector(
'[data-subscription-popup-channel-toggle-affordance]',
) as HTMLElement | null;
const browserCard = root.querySelector(
'[data-subscription-popup-channel-card="browser"]',
) as HTMLElement | null;
const emailCard = root.querySelector(
'[data-subscription-popup-channel-card="email"]',
) as HTMLElement | null;
const browserNote = root.querySelector(
'[data-subscription-popup-browser-note]',
) as HTMLElement | null;
const humanCheckCard = root.querySelector(
'[data-subscription-popup-human-check]',
) as HTMLElement | null;
const verificationMode = root.getAttribute('data-verification-mode') || 'off';
const useTurnstile = verificationMode === 'turnstile';
const useCaptcha = verificationMode === 'captcha';
const turnstileSiteKey = root.getAttribute('data-turnstile-site-key') || '';
const turnstileContainer = root.querySelector(
'[data-subscription-popup-turnstile]',
) as HTMLElement | null;
const turnstileTokenInput = form?.querySelector(
'input[name="turnstileToken"]',
) as HTMLInputElement | null;
const captchaQuestion = root.querySelector(
'[data-subscription-popup-captcha-question]',
) as HTMLElement | null;
const refreshCaptchaButton = root.querySelector(
'[data-subscription-popup-refresh-captcha]',
) as HTMLButtonElement | null;
const captchaTokenInput = form?.querySelector(
'input[name="captchaToken"]',
) as HTMLInputElement | null;
const captchaAnswerInput = form?.querySelector(
'input[name="captchaAnswer"]',
) as HTMLInputElement | null;
const pathname = window.location.pathname || '/';
const delayMs = Math.max(3000, Number(root.getAttribute('data-delay-ms') || '18000'));
const defaultStatus = status instanceof HTMLElement ? status.textContent?.trim() || '' : '';
const isAutomatedBrowser = window.navigator.webdriver === true;
if (
!(form instanceof HTMLFormElement) ||
!(status instanceof HTMLElement) ||
!(emailInput instanceof HTMLInputElement) ||
!(dismissButton instanceof HTMLButtonElement) ||
!combinedApiUrl ||
pathname.startsWith('/subscriptions/')
) {
return;
}
let opened = false;
let autoReady = false;
let engaged = false;
let autoOpened = false;
let hideTimer = 0;
let successTimer = 0;
let turnstileWidget: MountedTurnstile | null = null;
const browserRequired = browserCard instanceof HTMLElement;
let emailSelected = false;
let browserSubscribed = false;
let browserSelectable = browserRequired;
const header = document.querySelector('header');
const shouldFocusEmail = () =>
window.matchMedia('(hover: hover) and (pointer: fine)').matches;
const syncPopupOffset = () => {
const headerBottom =
header instanceof HTMLElement ? Math.round(header.getBoundingClientRect().bottom) : 0;
const offset = Math.max(14, headerBottom + 14);
root.style.setProperty('--subscription-popup-offset', `${offset}px`);
};
const hasDismissed = () => {
try {
return Number(window.localStorage.getItem(DISMISS_KEY) || '0') > Date.now();
} catch {
return false;
}
};
const hasSubmitted = () => {
try {
return Boolean(window.localStorage.getItem(SUBSCRIBED_KEY));
} catch {
return false;
}
};
const rememberDismiss = () => {
try {
window.localStorage.setItem(DISMISS_KEY, String(Date.now() + DISMISS_TTL_MS));
} catch {
// Ignore storage failures.
}
};
const rememberSubmitted = () => {
try {
window.localStorage.setItem(SUBSCRIBED_KEY, new Date().toISOString());
} catch {
// Ignore storage failures.
}
};
const forgetSubmitted = () => {
try {
window.localStorage.removeItem(SUBSCRIBED_KEY);
} catch {
// Ignore storage failures.
}
};
const resetStatus = () => {
delete status.dataset.state;
status.textContent = defaultStatus;
};
const setPending = (message: string) => {
status.dataset.state = 'pending';
status.textContent = message;
};
const setError = (message: string) => {
status.dataset.state = 'error';
status.textContent = message;
};
const setSuccess = (message: string) => {
status.dataset.state = 'success';
status.textContent = message;
};
const setSubmitButtonState = (label: string, disabled = false) => {
if (!(submitButton instanceof HTMLButtonElement)) {
return;
}
if (submitButtonLabel instanceof HTMLElement) {
submitButtonLabel.textContent = label;
} else {
submitButton.textContent = label;
}
submitButton.disabled = disabled;
};
const setToggleState = (
button: HTMLButtonElement | null,
active: boolean,
disabled = false,
) => {
if (!(button instanceof HTMLButtonElement)) {
return;
}
button.setAttribute('aria-pressed', String(active));
button.disabled = disabled;
button.classList.toggle('is-active', active);
button.classList.toggle('is-disabled', disabled);
};
const getPrimaryActionLabel = () => {
if (!browserRequired) {
return '提醒功能暂不可用';
}
if (emailSelected) {
return '开启提醒 + 邮箱备份';
}
return browserSubscribed ? '更新提醒设置' : '开启即时提醒';
};
const clearHumanCheckFields = () => {
if (turnstileTokenInput) {
turnstileTokenInput.value = '';
}
if (captchaTokenInput) {
captchaTokenInput.value = '';
}
if (captchaAnswerInput) {
captchaAnswerInput.value = '';
}
};
const syncChannelSelection = ({ focusEmail = false } = {}) => {
if (browserCard instanceof HTMLElement) {
browserCard.hidden = false;
}
if (emailCard instanceof HTMLElement) {
emailCard.hidden = !emailSelected;
}
if (humanCheckCard instanceof HTMLElement) {
humanCheckCard.hidden = !emailSelected || verificationMode === 'off';
}
emailInput.required = emailSelected;
if (emailToggleLabel instanceof HTMLElement) {
emailToggleLabel.textContent = emailSelected ? '收起邮件订阅' : '添加邮件订阅';
}
if (emailToggleDescription instanceof HTMLElement) {
emailToggleDescription.textContent = emailSelected
? '邮箱表单已经展开,填好后提交即可作为额外备份'
: '填写邮箱后,更新也会发到你的收件箱';
}
if (emailToggleAffordance instanceof HTMLElement) {
emailToggleAffordance.textContent = emailSelected ? '收起' : '去填写';
}
setToggleState(emailToggleButton, emailSelected, !browserRequired);
setSubmitButtonState(getPrimaryActionLabel(), !browserRequired || !browserSelectable);
if (emailSelected && opened) {
if (useTurnstile) {
void ensureTurnstile(false);
} else if (useCaptcha) {
void loadCaptcha(false);
}
} else if (!emailSelected) {
clearHumanCheckFields();
}
if (focusEmail && emailSelected && shouldFocusEmail()) {
emailInput.focus({ preventScroll: true });
}
};
const setBrowserAvailability = ({
selectable,
note,
subscribed = false,
}: {
selectable: boolean;
note: string;
subscribed?: boolean;
}) => {
browserSelectable = selectable;
browserSubscribed = subscribed;
if (browserNote instanceof HTMLElement) {
browserNote.textContent = note;
}
syncChannelSelection();
};
const openPopup = ({ focusEmail = false, force = false } = {}) => {
if (opened || (!force && hasSubmitted())) {
return;
}
syncPopupOffset();
window.clearTimeout(hideTimer);
if (status.dataset.state !== 'pending') {
resetStatus();
}
opened = true;
root.hidden = false;
window.requestAnimationFrame(() => {
root.classList.add('is-visible');
syncChannelSelection({ focusEmail });
});
};
const closePopup = (remember = true) => {
if (!opened) {
return;
}
opened = false;
window.clearTimeout(successTimer);
root.classList.remove('is-visible');
if (remember) {
rememberDismiss();
}
hideTimer = window.setTimeout(() => {
if (!opened) {
root.hidden = true;
}
}, 260);
};
const maybeAutoOpen = () => {
if (autoOpened || !autoReady || !engaged || hasDismissed() || hasSubmitted()) {
return;
}
autoOpened = true;
openPopup();
};
const markEngaged = () => {
engaged = true;
maybeAutoOpen();
};
const handleScroll = () => {
const doc = document.documentElement;
const maxScroll = Math.max(doc.scrollHeight - window.innerHeight, 1);
const progress = (window.scrollY / maxScroll) * 100;
if (progress >= 35) {
markEngaged();
window.removeEventListener('scroll', handleScroll);
}
};
const ensureTurnstile = async (showError = true) => {
if (!turnstileSiteKey || !turnstileContainer || !turnstileTokenInput) {
return;
}
turnstileTokenInput.value = '';
if (turnstileWidget) {
turnstileWidget.reset();
return;
}
try {
turnstileWidget = await mountTurnstile(turnstileContainer, {
siteKey: turnstileSiteKey,
onToken(token) {
turnstileTokenInput.value = token;
},
onExpire() {
turnstileTokenInput.value = '';
},
onError() {
turnstileTokenInput.value = '';
if (showError) {
setError('加载人机验证失败,请刷新页面后重试。');
}
},
});
} catch (error) {
if (showError) {
setError(error instanceof Error ? error.message : '加载人机验证失败,请刷新页面后重试。');
}
}
};
const loadCaptcha = async (showError = true) => {
if (!captchaQuestion || !captchaTokenInput || !captchaAnswerInput) {
return;
}
captchaQuestion.textContent = '加载中...';
captchaTokenInput.value = '';
captchaAnswerInput.value = '';
try {
const response = await fetch(captchaApiUrl);
if (!response.ok) {
throw new Error(await response.text());
}
const payload = (await response.json()) as { token?: string; question?: string };
captchaTokenInput.value = payload.token || '';
captchaQuestion.textContent = payload.question || '请刷新验证码';
} catch (error) {
captchaQuestion.textContent = '验证码加载失败,请刷新重试';
if (showError) {
setError(error instanceof Error ? error.message : '验证码加载失败,请刷新后重试。');
}
}
};
const resetHumanCheck = () => {
if (!emailSelected) {
clearHumanCheckFields();
return;
}
if (useTurnstile && turnstileTokenInput) {
turnstileTokenInput.value = '';
turnstileWidget?.reset();
return;
}
if (useCaptcha) {
void loadCaptcha(false);
}
};
const syncBrowserPushState = async () => {
if (!browserRequired) {
return;
}
if (!browserPushPublicKey) {
setBrowserAvailability({
selectable: false,
note: '当前提醒功能还没有准备好,因此现在无法开始订阅。',
});
return;
}
if (!supportsBrowserPush()) {
setBrowserAvailability({
selectable: false,
note: '当前设备暂时无法开启即时提醒,因此现在不能开始订阅。',
});
return;
}
try {
const { subscription, stale } = await getBrowserPushSubscriptionState(
browserPushPublicKey,
);
if (stale) {
forgetSubmitted();
setBrowserAvailability({
selectable: true,
note: '检测到提醒配置已更新,需要重新开启一次提醒。',
});
return;
}
if (subscription) {
rememberSubmitted();
setBrowserAvailability({
selectable: true,
subscribed: true,
note: '你已经开启了提醒;如果愿意,也可以再补一个邮箱备份。',
});
if (!opened) {
root.hidden = true;
}
} else if (Notification.permission === 'denied') {
setBrowserAvailability({
selectable: true,
note: '提醒功能当前没有开启;请先允许提醒后再试一次。',
});
} else {
setBrowserAvailability({
selectable: true,
note: '推荐默认使用这个方式;开启后新内容会更快提醒你。',
});
}
} catch {
setBrowserAvailability({
selectable: true,
note: '当前无法确认提醒状态,你仍然可以继续尝试开启。',
});
}
};
syncPopupOffset();
syncChannelSelection();
void syncBrowserPushState();
root.dataset.subscriptionPopupReady = 'true';
window.__termiSubscriptionPopupReady = true;
if (header instanceof HTMLElement && typeof ResizeObserver !== 'undefined') {
const observer = new ResizeObserver(() => syncPopupOffset());
observer.observe(header);
}
window.addEventListener('resize', syncPopupOffset, { passive: true });
if (!isAutomatedBrowser) {
window.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('pointerdown', markEngaged, { once: true, passive: true });
window.addEventListener('keydown', markEngaged, { once: true });
window.setTimeout(() => {
autoReady = true;
maybeAutoOpen();
}, delayMs);
}
document.addEventListener('click', (event) => {
const trigger =
event.target instanceof Element
? event.target.closest('[data-subscription-popup-open]')
: null;
if (!trigger) {
return;
}
event.preventDefault();
openPopup({ force: true });
});
root.querySelectorAll('[data-subscription-popup-close]').forEach((button) => {
button.addEventListener('click', () => closePopup(true));
});
dismissButton.addEventListener('click', () => closePopup(true));
refreshCaptchaButton?.addEventListener('click', () => {
void loadCaptcha(false);
});
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && opened) {
closePopup(true);
}
});
emailToggleButton?.addEventListener('click', () => {
if (!browserRequired) {
return;
}
if (emailSelected) {
emailSelected = false;
syncChannelSelection();
return;
}
emailSelected = true;
syncChannelSelection({ focusEmail: true });
});
form.addEventListener('submit', async (event) => {
event.preventDefault();
const formData = new FormData(form);
const email = String(formData.get('email') || '').trim();
const displayName = String(formData.get('displayName') || '').trim();
const wantsBrowser = browserRequired;
const wantsEmail = emailSelected;
if (!browserRequired) {
setError('当前暂时还不能开启即时提醒,请稍后再试。');
return;
}
if (!browserSelectable) {
setError('当前设备暂时无法开启提醒,请先允许提醒后再试。');
return;
}
if (wantsEmail && !email) {
setError('请输入邮箱地址。');
emailInput.focus();
return;
}
if (wantsEmail && useTurnstile) {
const token = String(formData.get('turnstileToken') || '').trim();
if (!token) {
setError('请先完成人机验证。');
return;
}
} else if (wantsEmail && useCaptcha) {
const captchaToken = String(formData.get('captchaToken') || '').trim();
const captchaAnswer = String(formData.get('captchaAnswer') || '').trim();
if (!captchaToken) {
setError('验证码加载失败,请刷新后重试。');
return;
}
if (!captchaAnswer) {
setError('请先填写验证码答案。');
captchaAnswerInput?.focus();
return;
}
}
let browserSubscription: unknown = undefined;
try {
if (wantsBrowser) {
if (!browserPushPublicKey) {
throw new Error('当前暂时还不能开启即时提醒,请稍后再试。');
}
if (!supportsBrowserPush()) {
throw new Error('当前设备暂时无法开启即时提醒。');
}
setPending(
wantsEmail ? '正在开启提醒,并准备邮箱备份...' : '正在开启提醒...',
);
setSubmitButtonState('处理中...', true);
browserSubscription = await ensureBrowserPushSubscription(browserPushPublicKey);
}
setPending(
wantsBrowser && wantsEmail
? '正在同时保存提醒和邮箱备份...'
: wantsBrowser
? '正在开启提醒...'
: '正在提交邮箱订阅...',
);
setSubmitButtonState('处理中...', true);
const response = await fetch(combinedApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
channels: [
...(wantsBrowser ? ['browser_push'] : []),
...(wantsEmail ? ['email'] : []),
],
email: wantsEmail ? email : undefined,
displayName: wantsEmail ? displayName : undefined,
subscription: wantsBrowser ? browserSubscription : undefined,
source: 'frontend-popup',
turnstileToken: wantsEmail ? formData.get('turnstileToken') : undefined,
captchaToken: wantsEmail ? formData.get('captchaToken') : undefined,
captchaAnswer: wantsEmail ? formData.get('captchaAnswer') : undefined,
}),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload?.message || payload?.description || '订阅失败,请稍后再试。');
}
rememberSubmitted();
browserSubscribed = Array.isArray(payload?.channels)
? payload.channels.some((item: { channel_type?: string }) => item?.channel_type === 'web_push')
: browserSubscribed || wantsBrowser;
form.reset();
resetHumanCheck();
syncChannelSelection();
setSuccess(
payload?.message ||
(wantsBrowser && wantsEmail
? '提醒已开启,邮箱备份也已提交。'
: wantsBrowser
? '提醒已开启,后续有新内容时会及时通知你。'
: '邮箱订阅已提交,请前往邮箱确认后生效。'),
);
successTimer = window.setTimeout(() => closePopup(false), 2200);
} catch (error) {
resetHumanCheck();
setError(error instanceof Error ? error.message : '订阅失败,请稍后重试。');
setSubmitButtonState(getPrimaryActionLabel(), false);
}
});
});
})();
</script>
<style>
.subscription-popup-root {
position: fixed;
inset: 0;
z-index: 90;
display: grid;
align-items: start;
justify-items: center;
padding: 0 max(1rem, env(safe-area-inset-right, 0px)) 0 max(1rem, env(safe-area-inset-left, 0px));
pointer-events: none;
}
.subscription-popup-root[hidden] {
display: none !important;
}
.subscription-popup-panel {
position: relative;
width: min(100%, 58rem);
display: grid;
gap: 1rem;
margin-top: var(--subscription-popup-offset, calc(env(safe-area-inset-top, 0px) + 5.25rem));
padding: 1.1rem;
padding-top: 3.5rem;
border-radius: 1.55rem;
opacity: 0;
transform: translateY(-1rem) scale(0.985);
transition:
opacity 0.24s ease,
transform 0.24s ease;
pointer-events: auto;
overflow: hidden;
backdrop-filter: blur(16px) saturate(135%);
background:
linear-gradient(
135deg,
rgba(var(--primary-rgb), 0.09),
rgba(var(--secondary-rgb, var(--primary-rgb)), 0.04) 42%,
transparent 72%
),
linear-gradient(
180deg,
color-mix(in oklab, var(--terminal-bg) 97%, transparent),
color-mix(in oklab, var(--header-bg) 92%, transparent)
);
box-shadow:
0 28px 70px rgba(var(--text-rgb), 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
.subscription-popup-panel::before {
content: '';
position: absolute;
inset: 0 0 auto;
height: 3px;
background:
linear-gradient(
90deg,
color-mix(in oklab, var(--primary) 88%, white),
color-mix(in oklab, var(--secondary) 80%, white)
);
opacity: 0.92;
}
.subscription-popup-root.is-visible .subscription-popup-panel {
opacity: 1;
transform: translateY(0) scale(1);
}
.subscription-popup-close {
position: absolute;
top: 0.8rem;
right: 0.8rem;
z-index: 3;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.15rem;
height: 2.15rem;
border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--primary) 16%, var(--border-color));
background: color-mix(in oklab, var(--header-bg) 90%, var(--terminal-bg));
color: var(--text-tertiary);
cursor: pointer;
transition:
border-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease;
box-shadow:
0 10px 22px rgba(var(--text-rgb), 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.subscription-popup-close:hover {
color: var(--title-color);
border-color: color-mix(in oklab, var(--primary) 24%, var(--border-color));
transform: translateY(-1px);
}
.subscription-popup-copy {
display: grid;
gap: 0.95rem;
min-width: 0;
}
.subscription-popup-main {
display: grid;
gap: 1rem;
position: relative;
z-index: 1;
}
.subscription-popup-copy-surface,
.subscription-popup-channel-card {
position: relative;
overflow: hidden;
border-radius: 1.2rem;
border: 1px solid color-mix(in oklab, var(--primary) 10%, var(--border-color));
background:
linear-gradient(
180deg,
color-mix(in oklab, var(--terminal-bg) 97%, transparent),
color-mix(in oklab, var(--header-bg) 91%, transparent)
);
box-shadow:
0 12px 30px rgba(var(--text-rgb), 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.subscription-popup-copy-surface {
padding: 1.15rem;
}
.subscription-popup-side {
display: grid;
gap: 0.9rem;
align-content: start;
}
.subscription-popup-side-intro {
display: grid;
gap: 0.85rem;
}
.subscription-popup-side-intro-copy {
display: grid;
gap: 0.28rem;
min-width: 0;
}
.subscription-popup-side-intro-copy h4 {
margin: 0;
color: var(--title-color);
font-size: 0.98rem;
line-height: 1.45;
}
.subscription-popup-side-intro-copy p:last-child {
margin: 0;
color: var(--text-tertiary);
font-size: 0.82rem;
line-height: 1.65;
}
.subscription-popup-channel-selector {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
width: 100%;
}
.subscription-popup-channel-toggle {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.8rem;
width: 100%;
min-height: 3.35rem;
border-radius: 1rem;
border: 1px solid color-mix(in oklab, var(--primary) 12%, var(--border-color));
background: color-mix(in oklab, var(--terminal-bg) 97%, transparent);
color: var(--text-secondary);
padding: 0.85rem 1rem;
text-align: left;
cursor: pointer;
transition:
border-color 0.2s ease,
color 0.2s ease,
background 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease;
}
.subscription-popup-channel-toggle-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.35rem;
height: 2.35rem;
flex-shrink: 0;
border-radius: 0.85rem;
border: 1px solid color-mix(in oklab, var(--primary) 12%, var(--border-color));
background: color-mix(in oklab, var(--primary) 10%, var(--terminal-bg));
color: var(--primary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
}
.subscription-popup-channel-toggle-copy {
display: grid;
gap: 0.18rem;
min-width: 0;
flex: 1;
}
.subscription-popup-channel-toggle-meta {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 0.1rem;
}
.subscription-popup-channel-toggle-tag {
display: inline-flex;
align-items: center;
border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--border-color) 88%, transparent);
background: color-mix(in oklab, var(--header-bg) 84%, transparent);
color: var(--text-tertiary);
padding: 0.15rem 0.45rem;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
.subscription-popup-channel-toggle-tag--email {
color: color-mix(in oklab, var(--secondary, var(--primary)) 62%, var(--title-color));
border-color: color-mix(
in oklab,
var(--secondary, var(--primary)) 22%,
var(--border-color)
);
background: color-mix(
in oklab,
var(--secondary, var(--primary)) 11%,
var(--terminal-bg)
);
}
.subscription-popup-channel-toggle-title {
display: block;
color: var(--title-color);
font-size: 0.92rem;
font-weight: 700;
line-height: 1.35;
}
.subscription-popup-channel-toggle-description {
display: block;
color: var(--text-tertiary);
font-size: 0.78rem;
line-height: 1.55;
}
.subscription-popup-channel-toggle-affordance {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin-left: auto;
align-self: center;
color: color-mix(in oklab, var(--secondary, var(--primary)) 58%, var(--title-color));
font-size: 0.78rem;
font-weight: 700;
line-height: 1;
white-space: nowrap;
transition:
color 0.2s ease,
transform 0.2s ease;
}
.subscription-popup-channel-toggle-affordance i {
font-size: 0.72rem;
transition: transform 0.2s ease;
}
.subscription-popup-channel-toggle:hover {
transform: translateY(-1px);
border-color: color-mix(in oklab, var(--primary) 22%, var(--border-color));
color: var(--title-color);
box-shadow: 0 14px 28px rgba(var(--text-rgb), 0.08);
}
.subscription-popup-channel-toggle:hover .subscription-popup-channel-toggle-affordance {
transform: translateX(1px);
}
.subscription-popup-channel-toggle.is-active {
border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));
background:
linear-gradient(
180deg,
color-mix(in oklab, var(--primary) 10%, var(--terminal-bg)),
color-mix(in oklab, var(--primary) 5%, var(--header-bg))
);
color: color-mix(in oklab, var(--primary) 76%, var(--title-color));
box-shadow:
inset 0 0 0 1px rgba(var(--primary-rgb), 0.08),
0 10px 22px rgba(var(--primary-rgb), 0.08);
}
.subscription-popup-channel-toggle.is-active .subscription-popup-channel-toggle-icon {
background: color-mix(in oklab, var(--primary) 16%, var(--terminal-bg));
}
.subscription-popup-channel-toggle.is-active .subscription-popup-channel-toggle-affordance i {
transform: rotate(90deg);
}
.subscription-popup-channel-toggle--primary {
border-color: color-mix(in oklab, var(--primary) 48%, var(--border-color));
background:
linear-gradient(
180deg,
color-mix(in oklab, var(--primary) 82%, white),
color-mix(in oklab, var(--primary) 72%, var(--header-bg))
);
color: white;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.24),
0 18px 38px rgba(var(--primary-rgb), 0.24);
}
.subscription-popup-channel-toggle--primary .subscription-popup-channel-toggle-icon {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(255, 255, 255, 0.14);
color: white;
}
.subscription-popup-channel-toggle--primary .subscription-popup-channel-toggle-title {
color: white;
}
.subscription-popup-channel-toggle--primary .subscription-popup-channel-toggle-description {
color: rgba(255, 255, 255, 0.82);
}
.subscription-popup-channel-toggle--primary:hover {
border-color: color-mix(in oklab, var(--primary) 68%, white);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.28),
0 20px 42px rgba(var(--primary-rgb), 0.28);
}
.subscription-popup-channel-toggle--email,
.subscription-popup-channel-toggle--email.is-active {
border-color: color-mix(
in oklab,
var(--secondary, var(--primary)) 42%,
var(--border-color)
);
background:
linear-gradient(
180deg,
color-mix(in oklab, var(--secondary, var(--primary)) 22%, var(--terminal-bg)),
color-mix(in oklab, var(--secondary, var(--primary)) 14%, var(--header-bg))
);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.3),
0 16px 34px rgba(var(--primary-rgb), 0.14);
}
.subscription-popup-channel-toggle--email .subscription-popup-channel-toggle-icon,
.subscription-popup-channel-toggle--email.is-active .subscription-popup-channel-toggle-icon {
border-color: color-mix(
in oklab,
var(--secondary, var(--primary)) 26%,
var(--border-color)
);
background: color-mix(
in oklab,
var(--secondary, var(--primary)) 18%,
var(--terminal-bg)
);
color: color-mix(
in oklab,
var(--secondary, var(--primary)) 70%,
var(--title-color)
);
}
.subscription-popup-channel-toggle--email .subscription-popup-channel-toggle-title,
.subscription-popup-channel-toggle--email.is-active .subscription-popup-channel-toggle-title {
color: color-mix(
in oklab,
var(--secondary, var(--primary)) 62%,
var(--title-color)
);
}
.subscription-popup-channel-toggle--email .subscription-popup-channel-toggle-description,
.subscription-popup-channel-toggle--email.is-active .subscription-popup-channel-toggle-description {
color: color-mix(
in oklab,
var(--secondary, var(--primary)) 34%,
var(--text-secondary)
);
}
.subscription-popup-channel-toggle--email:hover {
border-color: color-mix(
in oklab,
var(--secondary, var(--primary)) 56%,
var(--border-color)
);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.34),
0 18px 36px rgba(var(--primary-rgb), 0.16);
}
.subscription-popup-channel-toggle--ghost .subscription-popup-channel-toggle-icon {
background: color-mix(in oklab, var(--header-bg) 90%, white);
color: var(--text-secondary);
}
.subscription-popup-channel-toggle.is-disabled,
.subscription-popup-channel-toggle:disabled {
cursor: not-allowed;
opacity: 0.55;
transform: none;
box-shadow: none;
}
.subscription-popup-channel-helper {
margin: -0.05rem 0 0;
color: var(--text-tertiary);
font-size: 0.82rem;
line-height: 1.65;
}
.subscription-popup-copy-head {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 0.8rem;
}
.subscription-popup-copy-mark {
display: flex;
align-items: flex-start;
gap: 0.95rem;
min-width: 0;
padding-right: 0.25rem;
}
.subscription-popup-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
flex-shrink: 0;
border-radius: 1.1rem;
border: 1px solid color-mix(in oklab, var(--primary) 14%, var(--border-color));
background:
linear-gradient(
180deg,
color-mix(in oklab, var(--primary) 15%, var(--terminal-bg)),
color-mix(in oklab, var(--primary) 7%, var(--header-bg))
);
color: var(--primary);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.24),
0 14px 28px rgba(var(--primary-rgb), 0.12);
}
.subscription-popup-copy-body {
min-width: 0;
}
.subscription-popup-copy-body .terminal-kicker {
margin: 0 0 0.55rem;
}
.subscription-popup-copy h3 {
margin: 0;
font-size: 1.28rem;
line-height: 1.28;
color: var(--title-color);
}
.subscription-popup-copy p:last-child {
margin: 0.5rem 0 0;
color: var(--text-secondary);
line-height: 1.7;
font-size: 0.95rem;
}
.subscription-popup-meta {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
}
.subscription-popup-badges {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
padding-right: 2.55rem;
}
.subscription-popup-badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--primary) 12%, var(--border-color));
background: color-mix(in oklab, var(--primary) 6%, var(--terminal-bg));
color: var(--text-secondary);
padding: 0.28rem 0.7rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
white-space: nowrap;
}
.subscription-popup-points {
display: grid;
gap: 0.55rem;
margin: 1rem 0 0;
padding: 0;
list-style: none;
}
.subscription-popup-points li {
display: flex;
align-items: flex-start;
gap: 0.65rem;
color: var(--text-secondary);
font-size: 0.92rem;
line-height: 1.6;
}
.subscription-popup-point-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.3rem;
height: 1.3rem;
flex-shrink: 0;
border-radius: 999px;
background: color-mix(in oklab, var(--primary) 12%, var(--terminal-bg));
color: var(--primary);
font-size: 0.68rem;
margin-top: 0.05rem;
}
.subscription-popup-preview {
margin-top: 1.1rem;
}
.subscription-popup-preview-window {
position: relative;
overflow: hidden;
border-radius: 1.2rem;
border: 1px solid color-mix(in oklab, var(--primary) 12%, var(--border-color));
background:
linear-gradient(
180deg,
color-mix(in oklab, var(--header-bg) 94%, transparent),
color-mix(in oklab, var(--terminal-bg) 97%, transparent)
);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 14px 32px rgba(var(--text-rgb), 0.05);
}
.subscription-popup-preview-topbar {
display: flex;
gap: 0.38rem;
padding: 0.9rem 1rem 0;
}
.subscription-popup-preview-dot {
width: 0.56rem;
height: 0.56rem;
border-radius: 999px;
background: color-mix(in oklab, var(--primary) 18%, var(--border-color));
}
.subscription-popup-preview-card {
padding: 0.95rem 1rem 1rem;
display: grid;
gap: 0.45rem;
}
.subscription-popup-preview-kicker,
.subscription-popup-channel-kicker {
margin: 0;
color: var(--text-tertiary);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.subscription-popup-preview-card strong {
color: var(--title-color);
font-size: 1rem;
line-height: 1.4;
}
.subscription-popup-preview-card p {
margin: 0;
color: var(--text-secondary);
font-size: 0.87rem;
line-height: 1.7;
}
.subscription-popup-preview-tags {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
margin-top: 0.25rem;
}
.subscription-popup-preview-tags span {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 0.28rem 0.6rem;
background: color-mix(in oklab, var(--primary) 7%, var(--terminal-bg));
color: var(--text-secondary);
font-size: 0.72rem;
border: 1px solid color-mix(in oklab, var(--primary) 10%, var(--border-color));
}
.subscription-popup-channel-card {
display: grid;
gap: 0.85rem;
padding: 1rem;
}
.subscription-popup-channel-head {
display: flex;
align-items: center;
gap: 0.8rem;
min-width: 0;
}
.subscription-popup-channel-head h4 {
margin: 0.18rem 0 0;
color: var(--title-color);
font-size: 1rem;
line-height: 1.35;
}
.subscription-popup-channel-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0;
border-radius: 0.95rem;
border: 1px solid color-mix(in oklab, var(--primary) 14%, var(--border-color));
background: color-mix(in oklab, var(--primary) 10%, var(--terminal-bg));
color: var(--primary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
}
.subscription-popup-channel-icon--mail {
background: color-mix(in oklab, var(--secondary, var(--primary)) 10%, var(--terminal-bg));
color: color-mix(in oklab, var(--secondary, var(--primary)) 68%, var(--title-color));
}
.subscription-popup-channel-copy {
margin: 0;
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.65;
}
.subscription-popup-channel-note {
margin: -0.1rem 0 0;
color: var(--text-tertiary);
font-size: 0.8rem;
line-height: 1.65;
}
.subscription-popup-human-check {
margin-top: 0.15rem;
border-radius: 1.05rem;
border: 1px solid color-mix(in oklab, var(--primary) 10%, var(--border-color));
background: color-mix(in oklab, var(--header-bg) 88%, var(--terminal-bg));
padding: 0.9rem 0.95rem;
}
.subscription-popup-form {
display: grid;
gap: 0.8rem;
}
.subscription-popup-form [hidden] {
display: none !important;
}
.subscription-popup-field {
display: grid;
gap: 0.45rem;
min-width: 0;
}
.subscription-popup-field-label {
color: var(--text-tertiary);
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.subscription-popup-form input {
width: 100%;
min-height: 3rem;
border-radius: 1rem;
border: 1px solid color-mix(in oklab, var(--primary) 12%, var(--border-color));
background: color-mix(in oklab, var(--terminal-bg) 96%, transparent);
color: var(--title-color);
padding: 0.9rem 1rem;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
background 0.2s ease;
}
.subscription-popup-form input::placeholder {
color: var(--text-tertiary);
}
.subscription-popup-form input:focus-visible {
outline: none;
border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));
background: color-mix(in oklab, var(--terminal-bg) 98%, transparent);
box-shadow: 0 0 0 4px rgba(var(--primary-rgb), 0.08);
}
.subscription-popup-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
margin-top: 0.1rem;
align-items: stretch;
}
.subscription-popup-actions .subscription-popup-channel-toggle {
min-height: 3.4rem;
}
.subscription-popup-primary-button {
box-shadow:
0 12px 24px rgba(var(--primary-rgb), 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.28);
}
.subscription-popup-status {
margin: 0;
padding: 0.85rem 1rem;
border-radius: 1rem;
border: 1px solid color-mix(in oklab, var(--primary) 10%, var(--border-color));
background: color-mix(in oklab, var(--primary) 5%, var(--terminal-bg));
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.7;
}
.subscription-popup-status[data-state='pending'] {
color: var(--primary);
border-color: color-mix(in oklab, var(--primary) 26%, var(--border-color));
background: color-mix(in oklab, var(--primary) 8%, var(--terminal-bg));
}
.subscription-popup-status[data-state='success'] {
color: var(--success-dark);
border-color: color-mix(in oklab, var(--success) 24%, var(--border-color));
background: color-mix(in oklab, var(--success) 10%, var(--terminal-bg));
}
.subscription-popup-status[data-state='error'] {
color: var(--danger);
border-color: color-mix(in oklab, var(--danger) 24%, var(--border-color));
background: color-mix(in oklab, var(--danger) 8%, var(--terminal-bg));
}
@media (min-width: 960px) {
.subscription-popup-main {
grid-template-columns: minmax(0, 1.08fr) minmax(19rem, 0.92fr);
align-items: start;
gap: 1rem 1.15rem;
}
.subscription-popup-panel {
padding: 1.15rem;
}
.subscription-popup-status {
grid-column: 1 / -1;
}
}
@media (max-width: 767px) {
.subscription-popup-root {
padding-inline: 0.75rem;
}
.subscription-popup-panel {
gap: 0.9rem;
padding: 3.1rem 0.9rem 0.95rem;
}
.subscription-popup-copy-mark {
gap: 0.75rem;
padding-right: 0;
}
.subscription-popup-icon {
width: 2.65rem;
height: 2.65rem;
border-radius: 0.9rem;
}
.subscription-popup-badges {
padding-right: 2.15rem;
}
.subscription-popup-copy-surface,
.subscription-popup-channel-card {
border-radius: 1.05rem;
}
.subscription-popup-copy-surface {
padding: 1rem;
}
.subscription-popup-actions {
grid-template-columns: 1fr;
}
.subscription-popup-actions .subscription-popup-channel-toggle {
width: 100%;
}
}
</style>