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
1907 lines
60 KiB
Plaintext
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>
|