fix(pricing): align Go plan launch experience (#7167)

* fix(pricing): align Go plan launch experience

* test(e2e): dismiss Go launch campaign in UI specs

* fix(go-plan): finish launch campaign polish

* test(e2e): preserve Go campaign visual coverage

---------

Co-authored-by: Amy <1184569493@qq.com>
This commit is contained in:
Ray Xi
2026-08-20 10:57:59 +00:00
committed by GitHub
co-authored by Amy
parent 90741ab238
commit 5459ffbc0f
28 changed files with 1135 additions and 389 deletions
@@ -21,7 +21,7 @@ const apiOrigin =
<aside
class="go-banner"
data-go-banner
data-window-start="2026-08-20T20:00:00+08:00"
data-window-start="2026-08-20T17:00:00+08:00"
data-window-end="2026-09-03T20:00:00+08:00"
data-api-origin={apiOrigin}
aria-label={copy.ariaLabel}
@@ -38,27 +38,27 @@ const DEMO_ASSET_BASE = '/pricing-e-final/assets';
const popularModels: ModelItem[] = [
{ name: 'DeepSeek V4 Flash', icon: '/agents/deepseek.svg' },
{ name: 'GLM-5.2', icon: `${DEMO_ASSET_BASE}/zai-logo-official-Byn-xbrp.png`, iconClass: 'model-logo-zhipu' },
{ name: 'Kimi K2.7 Code', icon: '/agents/moonshot.svg', iconClass: 'model-logo-kimi' },
{ name: 'Kimi K2.7 Code', icon: '/model-icons/kimi.svg', iconClass: 'model-logo-kimi' },
{ name: 'DeepSeek V4 Pro', icon: '/agents/deepseek.svg' },
{ name: 'MiniMax M2.7', icon: '/agents/minimax.svg' },
{ name: 'Kimi K2.6', icon: '/agents/moonshot.svg', iconClass: 'model-logo-kimi' },
{ name: 'MiniMax M2.7', icon: '/model-icons/minimax.svg' },
{ name: 'Kimi K2.6', icon: '/model-icons/kimi.svg', iconClass: 'model-logo-kimi' },
{ name: 'MiMo V2.5 Pro', icon: `${DEMO_ASSET_BASE}/mimo-logo-user-CWOWEwG5.png`, iconClass: 'model-logo-mimo' },
{ name: 'GLM-5.1', icon: `${DEMO_ASSET_BASE}/zai-logo-official-Byn-xbrp.png`, iconClass: 'model-logo-zhipu' },
];
const flagshipModels: ModelItem[] = [
{ name: 'Claude Fable 5', icon: '/agents/anthropic.svg' },
{ name: 'Kimi-K3', icon: '/agents/moonshot.svg', iconClass: 'model-logo-kimi' },
{ name: 'Claude Fable 5', icon: '/model-icons/claude.svg' },
{ name: 'Kimi-K3', icon: '/model-icons/kimi.svg', iconClass: 'model-logo-kimi' },
{ name: 'GPT-5.6-Sol', icon: '/agents/openai.svg' },
{ name: 'Claude Opus 5', icon: '/agents/anthropic.svg' },
{ name: 'Claude Opus 5', icon: '/model-icons/claude.svg' },
{ name: 'GPT-5.5-Pro', icon: '/agents/openai.svg' },
{ name: 'Claude Opus 4.7', icon: '/agents/anthropic.svg' },
{ name: 'Claude Opus 4.8', icon: '/agents/anthropic.svg' },
{ name: 'Claude Sonnet 5', icon: '/agents/anthropic.svg' },
{ name: 'Claude Opus 4.6', icon: '/agents/anthropic.svg' },
{ name: 'Claude Sonnet 4.6', icon: '/agents/anthropic.svg' },
{ name: 'Claude Opus 4.7', icon: '/model-icons/claude.svg' },
{ name: 'Claude Opus 4.8', icon: '/model-icons/claude.svg' },
{ name: 'Claude Sonnet 5', icon: '/model-icons/claude.svg' },
{ name: 'Claude Opus 4.6', icon: '/model-icons/claude.svg' },
{ name: 'Claude Sonnet 4.6', icon: '/model-icons/claude.svg' },
{ name: 'GPT-5.5', icon: '/agents/openai.svg' },
{ name: 'Grok-4.5', icon: '/agents/xai.svg' },
{ name: 'Grok-4.5', icon: '/model-icons/grok.svg' },
];
const plusFlagshipModels = flagshipModels.filter(
@@ -66,8 +66,8 @@ const plusFlagshipModels = flagshipModels.filter(
);
const imageModels: ModelItem[] = [
{ name: 'Seedream 5 Lite / Pro', icon: '/model-icons/bytedance.svg' },
{ name: 'Nano Banana 2', icon: '/agents/gemini.svg', iconClass: 'model-logo-nanobanana' },
{ name: 'Seedream 5 Lite / Pro', icon: '/model-icons/jimeng.svg' },
{ name: 'Nano Banana 2', icon: '/model-icons/nanobanana.svg', iconClass: 'model-logo-nanobanana' },
{ name: 'GPT Image 2', icon: '/agents/openai.svg' },
];
@@ -155,6 +155,15 @@ const tierBenefits: Record<TierId, string[]> = {
max: paidTierBenefits('max'),
};
const compactSavings = (amountUsd: number, interval: 'monthly' | 'yearly') => {
const amount = locale === 'en'
? formatUsd(amountUsd).replace('$', '')
: formatUsd(amountUsd);
return locale === 'en' && interval === 'monthly' ? `${amount}/mo` : amount;
};
const compactBillingTotal = (amountUsd: number) =>
locale === 'en' ? formatUsd(amountUsd).replace('$', '') : formatUsd(amountUsd);
function individualView(tier: PlanTierConfig) {
const yearlyEquivalent = Math.round(tier.yearly.priceUsd / 12);
const yearlySave = tier.monthly.priceUsd * 12 - tier.yearly.priceUsd;
@@ -166,7 +175,10 @@ function individualView(tier: PlanTierConfig) {
amount: formatUsd(tier.monthly.introPriceUsd).replace('$', ''),
strike: formatUsd(tier.monthly.priceUsd),
discount: introDiscount,
saving: formatUsd(tier.monthly.priceUsd - tier.monthly.introPriceUsd),
saving: compactSavings(
tier.monthly.priceUsd - tier.monthly.introPriceUsd,
'monthly',
),
billed: fillTemplate(L.monthlyRenewal, {
amountUsd: formatUsd(tier.monthly.priceUsd),
}),
@@ -175,9 +187,9 @@ function individualView(tier: PlanTierConfig) {
amount: formatUsd(yearlyEquivalent).replace('$', ''),
strike: formatUsd(tier.monthly.priceUsd),
discount: tier.yearly.discountPct,
saving: formatUsd(yearlySave),
saving: compactSavings(yearlySave, 'yearly'),
billed: fillTemplate(L.yearlySubline, {
totalUsd: formatUsd(tier.yearly.priceUsd),
totalUsd: compactBillingTotal(tier.yearly.priceUsd),
savingsUsd: formatUsd(yearlySave),
}),
},
@@ -197,7 +209,10 @@ const goView = {
amount: formatUsd(GO_PLAN.monthly.introPriceUsd).replace('$', ''),
strike: formatUsd(GO_PLAN.monthly.priceUsd),
discount: goMonthlyDiscount,
saving: formatUsd(GO_PLAN.monthly.priceUsd - GO_PLAN.monthly.introPriceUsd),
saving: compactSavings(
GO_PLAN.monthly.priceUsd - GO_PLAN.monthly.introPriceUsd,
'monthly',
),
billed: fillTemplate(L.monthlyRenewal, {
amountUsd: formatUsd(GO_PLAN.monthly.priceUsd),
}),
@@ -206,9 +221,9 @@ const goView = {
amount: formatUsd(goYearlyEquivalent).replace('$', ''),
strike: formatUsd(GO_PLAN.monthly.priceUsd),
discount: goYearlyDiscount,
saving: formatUsd(goYearlySave),
saving: compactSavings(goYearlySave, 'yearly'),
billed: fillTemplate(L.yearlySubline, {
totalUsd: formatUsd(GO_PLAN.yearly.priceUsd),
totalUsd: compactBillingTotal(GO_PLAN.yearly.priceUsd),
savingsUsd: formatUsd(goYearlySave),
}),
},
@@ -241,11 +256,11 @@ const usageRows = [
const comparisonPopular = [
'DeepSeek V4 Flash',
'Kimi K2.7 Code',
'DeepSeek V4 Pro',
'GLM-5.2',
'Kimi K2.7 Code',
'MiniMax M2.7',
'MiMo V2.5 Pro',
'GLM-5.2',
'Kimi K2.6',
'GLM-5.1',
].map((name) => popularModels.find((model) => model.name === name)!);
@@ -368,8 +383,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
</div>
<a class="pricing-card-cta" href={cloudSubscribeUrl(tier, 'yearly')} data-pricing-cta data-tier={tier}>
<span data-when="monthly">{tierCopy[tier].ctaLabel} · {L.monthly}</span>
<span data-when="yearly">{tierCopy[tier].ctaLabel} · {L.yearly}</span>
<span>{tierCopy[tier].ctaLabel}</span>
</a>
<div class="plan-feature-details">
@@ -385,7 +399,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
<li class="model-with-status">
<span class:list={['model-logo', model.iconClass]}><img src={model.icon} alt="" loading="lazy" /></span>
<span>{model.name}</span>
<em class={popularStatus.kind}>{popularStatus.text}</em>
{popularStatus.kind === 'unlimited' && <em class={popularStatus.kind}>{popularStatus.text}</em>}
</li>
);
})}
@@ -397,7 +411,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
<section class:list={['plan-model-module', { 'unavailable-model-module': tier === 'go', 'recommended-model-module': tier === 'pro' || tier === 'max' }]} data-model-module>
<header>
<h4>{tier === 'go' ? P.flagshipModels : `${fillTemplate(P.modelCount, { count: `${flagship.length}+` })} · ${P.flagshipModels}`}</h4>
<h4>{tier === 'go' ? P.flagshipModels : fillTemplate(P.flagshipModelCount, { count: String(flagship.length) })}</h4>
{tier === 'go' && <span>{P.noAccess}</span>}
</header>
<ul>
@@ -480,7 +494,6 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
</div>
<div class="individual-usage-note">
<p>{P.usageAllowanceNote}</p>
<p>{P.usagePeakNote}</p>
<p>{P.usageEstimatesNote}</p>
</div>
</section>
@@ -529,7 +542,11 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
<td class="model-name-cell"><span class:list={['model-logo', model.iconClass]}><img src={model.icon} alt="" loading="lazy" /></span><span>{model.name}</span></td>
{(['go', 'plus', 'pro', 'max'] as TierId[]).map((tier) => {
const status = accessStatus(category, model.name, tier);
return <td><span class={`model-access-status ${status.kind}`}><i class:list={['status-icon', { infinity: status.kind === 'unlimited', check: status.kind === 'more-ample' }]}>{status.kind === 'unlimited' ? '∞' : ''}</i>{status.text}</span></td>;
return status.kind === 'more-ample' ? (
<td><span class={`model-access-status ${status.kind}`} aria-label={status.text}><i class="status-icon check"></i></span></td>
) : (
<td><span class={`model-access-status ${status.kind}`}><i class:list={['status-icon', { infinity: status.kind === 'unlimited' }]}>{status.kind === 'unlimited' ? '∞' : ''}</i>{status.text}</span></td>
);
})}
</tr>
)),
@@ -630,7 +647,7 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
padding: 0 12px;
overflow: hidden;
color: #173111;
font-size: 9px;
font-size: 11px;
font-weight: 650;
line-height: 1;
letter-spacing: 0.055em;
@@ -640,12 +657,19 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
}
.new-plan-ribbon span {
position: relative;
z-index: 1;
color: #173111;
background: linear-gradient(110deg, #173111 0% 39%, #397f2b 47%, #d8ff70 52%, #397f2b 57%, #173111 65% 100%) 100% 0 / 300% 100%;
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
animation: pricing-ribbon-text-shimmer 4.2s linear infinite;
}
.plan-top {
display: block;
min-height: 58px;
min-height: 78px;
}
.plan-title-row {
@@ -708,12 +732,14 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
.plan-top p {
max-width: none;
min-height: 32px;
margin: 8px 0 0;
color: #60645c;
font-size: 12px;
font-weight: 400;
line-height: 1.35;
white-space: nowrap;
white-space: normal;
overflow-wrap: anywhere;
}
.plan-max .plan-top p { color: #aeb5aa; }
@@ -758,10 +784,11 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
.plan-max .price del { color: #8f978b; }
.compact-price-detail {
display: flex;
align-items: center;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: start;
gap: 7px;
min-height: 34px;
min-height: 46px;
color: #555a52;
}
@@ -774,11 +801,14 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
}
.compact-price-detail > small {
margin-left: auto;
min-width: 0;
margin-left: 0;
color: #51554d;
font-size: 12px;
font-weight: 400;
white-space: nowrap;
text-align: right;
white-space: normal;
overflow-wrap: anywhere;
}
.plan-max .compact-price-detail > span.price-savings-note { color: #83d96c; }
@@ -1531,8 +1561,11 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
white-space: nowrap;
}
.model-access-status.unlimited,
.model-access-status.more-ample { gap: 8px; }
.model-access-status.unlimited { gap: 8px; }
.model-access-status.more-ample {
width: 23px;
height: 23px;
}
.model-access-status.resolution { font-weight: 500; }
.status-icon {
@@ -1636,6 +1669,10 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
from { opacity: 0.4; transform: scaleX(0); }
to { opacity: 1; transform: scaleX(1); }
}
@keyframes pricing-ribbon-text-shimmer {
from { background-position: 100% 0; }
to { background-position: -100% 0; }
}
@media (max-width: 1080px) {
.four-tier-grid {
@@ -1669,6 +1706,12 @@ const otherModels = 'Gemini-3.1-Pro, Grok-4.5, GPT-5.4, GPT-5.6-Terra, Qwen3.7-M
animation: none;
transform: none;
}
.new-plan-ribbon span {
color: #173111;
-webkit-text-fill-color: #173111;
background: none;
animation: none;
}
.pricing-card-cta { transition: none; }
}
</style>
@@ -9,6 +9,8 @@ export interface PricingCampaignContent {
modelBenefits: readonly [string, string];
paidBenefitNote: string;
teamBenefitNote: string;
teamOfferTitle: string;
teamOfferModels: string;
disclaimer: string;
linkLabel: string;
closeLabel: string;
@@ -24,6 +26,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['Unlimited DeepSeek V4 Pro', 'Unlimited DeepSeek V4 Flash'],
paidBenefitNote: 'Aug 13—Aug 27 · FREE for two weeks',
teamBenefitNote: 'Aug 13—Aug 27 · FREE for two weeks',
teamOfferTitle: 'Unlimited model access',
teamOfferModels: 'DeepSeek V4 Flash and V4 Pro',
disclaimer: 'Unlimited model quota and free generations included in a plan are available only in OpenDesign; they cannot be used through MCP/CLI/API or in other scenarios. Some models may require queuing during peak hours. The organizer reserves the right of final interpretation.',
linkLabel: 'View campaign benefits', closeLabel: 'Dismiss campaign banner',
},
@@ -36,6 +40,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['DeepSeek V4 Pro 无限使用', 'DeepSeek V4 Flash 无限使用'],
paidBenefitNote: '8月13日—8月27日 · 两周免费用',
teamBenefitNote: '8月13日—8月27日 · 两周免费用',
teamOfferTitle: '无限模型使用权益',
teamOfferModels: 'DeepSeek V4 Flash 与 V4 Pro',
disclaimer: '套餐内的无限制模型额度与免费生成次数,仅可通过OpenDesign使用;无法在MCP/CLI/API及其他场景使用。部分模型高峰期需要排队。解释权归官方所有。',
linkLabel: '查看活动权益', closeLabel: '关闭活动横幅',
},
@@ -48,6 +54,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['DeepSeek V4 Pro 無限使用', 'DeepSeek V4 Flash 無限使用'],
paidBenefitNote: '8月13日—8月27日 · 兩週免費用',
teamBenefitNote: '8月13日—8月27日 · 兩週免費用',
teamOfferTitle: '無限模型使用權益',
teamOfferModels: 'DeepSeek V4 Flash 與 V4 Pro',
disclaimer: '方案內的無限制模型額度與免費生成次數,僅可透過 OpenDesign 使用;無法在 MCP/CLI/API 及其他場景使用。部分模型在高峰時段可能需要排隊。最終解釋權歸官方所有。',
linkLabel: '查看活動權益', closeLabel: '關閉活動橫幅',
},
@@ -60,6 +68,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['DeepSeek V4 Proを無制限で利用', 'DeepSeek V4 Flashを無制限で利用'],
paidBenefitNote: '8月13日〜8月27日 · 2週間無料',
teamBenefitNote: '8月13日〜8月27日 · 2週間無料',
teamOfferTitle: 'モデルを無制限に利用',
teamOfferModels: 'DeepSeek V4 Flash と V4 Pro',
disclaimer: 'プランに含まれる無制限のモデル枠と無料生成回数は、OpenDesign内でのみ利用できます。MCP/CLI/APIなど、その他の環境では利用できません。一部のモデルはピーク時に待ち時間が発生する場合があります。最終的な解釈権は運営者に帰属します。',
linkLabel: '特典を見る', closeLabel: 'キャンペーンバナーを閉じる',
},
@@ -72,6 +82,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['DeepSeek V4 Pro 무제한 사용', 'DeepSeek V4 Flash 무제한 사용'],
paidBenefitNote: '8월 13일—8월 27일 · 2주 무료',
teamBenefitNote: '8월 13일—8월 27일 · 2주 무료',
teamOfferTitle: '모델 무제한 사용',
teamOfferModels: 'DeepSeek V4 Flash 및 V4 Pro',
disclaimer: '플랜에 포함된 무제한 모델 한도와 무료 생성 횟수는 OpenDesign에서만 사용할 수 있으며 MCP/CLI/API 또는 기타 환경에서는 사용할 수 없습니다. 일부 모델은 피크 시간대에 대기해야 할 수 있습니다. 최종 해석 권한은 운영사에 있습니다.',
linkLabel: '이벤트 혜택 보기', closeLabel: '이벤트 배너 닫기',
},
@@ -84,6 +96,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['DeepSeek V4 Pro unbegrenzt nutzen', 'DeepSeek V4 Flash unbegrenzt nutzen'],
paidBenefitNote: '13.—27. August · zwei Wochen kostenlos',
teamBenefitNote: '13.—27. August · zwei Wochen kostenlos',
teamOfferTitle: 'Unbegrenzter Modellzugang',
teamOfferModels: 'DeepSeek V4 Flash und V4 Pro',
disclaimer: 'Das im Tarif enthaltene unbegrenzte Modellkontingent und die kostenlosen Generierungen können nur in OpenDesign genutzt werden, nicht über MCP/CLI/API oder in anderen Umgebungen. Bei einigen Modellen kann es zu Spitzenzeiten zu Wartezeiten kommen. Der Veranstalter behält sich die endgültige Auslegung vor.',
linkLabel: 'Aktionsvorteile ansehen', closeLabel: 'Aktionsbanner schließen',
},
@@ -96,6 +110,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['DeepSeek V4 Pro en illimité', 'DeepSeek V4 Flash en illimité'],
paidBenefitNote: 'Du 13 au 27 août · gratuits pendant deux semaines',
teamBenefitNote: 'Du 13 au 27 août · gratuits pendant deux semaines',
teamOfferTitle: 'Accès illimité aux modèles',
teamOfferModels: 'DeepSeek V4 Flash et V4 Pro',
disclaimer: 'Le quota de modèles illimité et les générations gratuites inclus dans le forfait sont utilisables uniquement dans OpenDesign, et non via MCP/CLI/API ni dans d’autres contextes. Certains modèles peuvent nécessiter une mise en file d’attente aux heures de pointe. L’organisateur se réserve le droit d’interprétation finale.',
linkLabel: 'Voir les avantages de la campagne', closeLabel: 'Fermer la bannière',
},
@@ -108,6 +124,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['DeepSeek V4 Pro без ограничений', 'DeepSeek V4 Flash без ограничений'],
paidBenefitNote: '13—27 августа · две недели бесплатно',
teamBenefitNote: '13—27 августа · две недели бесплатно',
teamOfferTitle: 'Безлимитный доступ к моделям',
teamOfferModels: 'DeepSeek V4 Flash и V4 Pro',
disclaimer: 'Безлимитная квота моделей и бесплатные генерации, включённые в тариф, доступны только в OpenDesign. Они недоступны через MCP/CLI/API или в других сценариях. Для некоторых моделей в часы пик может потребоваться ожидание в очереди. Организатор оставляет за собой право окончательного толкования.',
linkLabel: 'Посмотреть преимущества', closeLabel: 'Закрыть баннер',
},
@@ -120,6 +138,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['Uso ilimitado de DeepSeek V4 Pro', 'Uso ilimitado de DeepSeek V4 Flash'],
paidBenefitNote: 'Del 13 al 27 de agosto · gratis durante dos semanas',
teamBenefitNote: 'Del 13 al 27 de agosto · gratis durante dos semanas',
teamOfferTitle: 'Acceso ilimitado a modelos',
teamOfferModels: 'DeepSeek V4 Flash y V4 Pro',
disclaimer: 'La cuota ilimitada de modelos y las generaciones gratuitas incluidas en el plan solo pueden utilizarse en OpenDesign, no mediante MCP/CLI/API ni en otros entornos. Algunos modelos pueden requerir espera en horas punta. El organizador se reserva el derecho de interpretación final.',
linkLabel: 'Ver beneficios', closeLabel: 'Cerrar el banner',
},
@@ -132,6 +152,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['Uso ilimitado do DeepSeek V4 Pro', 'Uso ilimitado do DeepSeek V4 Flash'],
paidBenefitNote: '13 a 27 de agosto · grátis por duas semanas',
teamBenefitNote: '13 a 27 de agosto · grátis por duas semanas',
teamOfferTitle: 'Acesso ilimitado aos modelos',
teamOfferModels: 'DeepSeek V4 Flash e V4 Pro',
disclaimer: 'A cota ilimitada de modelos e as gerações gratuitas incluídas no plano só podem ser usadas no OpenDesign, e não via MCP/CLI/API nem em outros cenários. Alguns modelos podem exigir espera em horários de pico. O organizador se reserva o direito de interpretação final.',
linkLabel: 'Ver benefícios', closeLabel: 'Fechar banner',
},
@@ -144,6 +166,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['DeepSeek V4 Pro senza limiti', 'DeepSeek V4 Flash senza limiti'],
paidBenefitNote: '13—27 agosto · gratis per due settimane',
teamBenefitNote: '13—27 agosto · gratis per due settimane',
teamOfferTitle: 'Accesso illimitato ai modelli',
teamOfferModels: 'DeepSeek V4 Flash e V4 Pro',
disclaimer: 'La quota modelli illimitata e le generazioni gratuite incluse nel piano sono utilizzabili solo in OpenDesign, non tramite MCP/CLI/API né in altri contesti. Alcuni modelli potrebbero richiedere attesa nelle ore di punta. L’organizzatore si riserva il diritto di interpretazione finale.',
linkLabel: 'Scopri i vantaggi', closeLabel: 'Chiudi il banner',
},
@@ -156,6 +180,8 @@ export const PRICING_CAMPAIGN_CONTENT_BY_LOCALE = {
modelBenefits: ['DeepSeek V4 Pro sınırsız kullanım', 'DeepSeek V4 Flash sınırsız kullanım'],
paidBenefitNote: '13—27 Ağustos · iki hafta ücretsiz',
teamBenefitNote: '13—27 Ağustos · iki hafta ücretsiz',
teamOfferTitle: 'Sınırsız model erişimi',
teamOfferModels: 'DeepSeek V4 Flash ve V4 Pro',
disclaimer: 'Paket kapsamındaki sınırsız model kotası ve ücretsiz üretim hakları yalnızca OpenDesign içinde kullanılabilir; MCP/CLI/API veya diğer senaryolarda kullanılamaz. Bazı modeller yoğun saatlerde sıraya alınabilir. Nihai yorum hakkı organizatöre aittir.',
linkLabel: 'Kampanya avantajlarını gör', closeLabel: 'Kampanya bandını kapat',
},
+21 -21
View File
@@ -105,6 +105,7 @@ export interface PersonalPricingCopy {
creatorDeveloperSupport: string;
popularModels: string;
flagshipModels: string;
flagshipModelCount: string;
imageModels: string;
modelCount: string;
unlimited: string;
@@ -122,7 +123,6 @@ export interface PersonalPricingCopy {
estimatedRequests: string;
estimatedRequestsAria: string;
usageAllowanceNote: string;
usagePeakNote: string;
usageEstimatesNote: string;
comparisonTitle: string;
category: string;
@@ -244,6 +244,7 @@ const PERSONAL_EN: PersonalPricingCopy = {
creatorDeveloperSupport: 'Creator / developer support',
popularModels: 'Popular models',
flagshipModels: 'Flagship models',
flagshipModelCount: '{count}+ flagship models',
imageModels: 'Image models',
modelCount: '{count} models',
unlimited: 'Unlimited',
@@ -261,7 +262,6 @@ const PERSONAL_EN: PersonalPricingCopy = {
estimatedRequests: 'Estimated requests every 5 hours',
estimatedRequestsAria: 'Estimated model requests every 5 hours',
usageAllowanceNote: 'Popular models include ample allowance, roughly equivalent to $16 every 5 hours · $40 weekly · $80 monthly. Actual usage varies by model and context length.',
usagePeakNote: 'DeepSeek V4 Flash / Pro estimates use off-peak pricing; unit usage counts ×2 during peak hours (Beijing time 09:00–12:00 and 14:00–18:00).',
usageEstimatesNote: 'Estimated request counts by model may change with model pricing, usage patterns, and user feedback.',
comparisonTitle: 'Compare every model across plans',
category: 'Category',
@@ -289,6 +289,7 @@ const PERSONAL_ZH_CN: PersonalPricingCopy = {
creatorDeveloperSupport: '创作者 / 开发者支持',
popularModels: '热门模型',
flagshipModels: '旗舰模型',
flagshipModelCount: '{count}+ 个旗舰模型',
imageModels: '图片模型',
modelCount: '{count} 个模型',
unlimited: '无限量',
@@ -306,7 +307,6 @@ const PERSONAL_ZH_CN: PersonalPricingCopy = {
estimatedRequests: '每 5 小时预估可用次数',
estimatedRequestsAria: '模型每 5 小时预估请求数',
usageAllowanceNote: '热门模型提供充足额度,大致相当于每 5 小时 $16 · 每周 $40 · 每月 $80。实际用量因模型和上下文长度而异。',
usagePeakNote: 'DeepSeek V4 Flash / Pro 按非高峰价格估算;高峰时段(北京时间 09:00–12:00、14:00–18:00)单位消耗 ×2。',
usageEstimatesNote: '各模型的预估次数可能随模型价格、使用情况与用户反馈调整。',
comparisonTitle: '对比各套餐的全部模型',
category: '分类',
@@ -334,6 +334,7 @@ const PERSONAL_ZH_TW: PersonalPricingCopy = {
creatorDeveloperSupport: '創作者 / 開發者支援',
popularModels: '熱門模型',
flagshipModels: '旗艦模型',
flagshipModelCount: '{count}+ 個旗艦模型',
imageModels: '圖片模型',
modelCount: '{count} 個模型',
unlimited: '無限量',
@@ -351,7 +352,6 @@ const PERSONAL_ZH_TW: PersonalPricingCopy = {
estimatedRequests: '每 5 小時預估可用次數',
estimatedRequestsAria: '模型每 5 小時預估請求數',
usageAllowanceNote: '熱門模型提供充足額度,大致相當於每 5 小時 $16 · 每週 $40 · 每月 $80。實際用量依模型與上下文長度而異。',
usagePeakNote: 'DeepSeek V4 Flash / Pro 依離峰價格估算;尖峰時段(北京時間 09:00–12:00、14:00–18:00)單位用量 ×2。',
usageEstimatesNote: '各模型的預估次數可能依模型價格、使用情況與使用者回饋調整。',
comparisonTitle: '比較各方案的全部模型',
category: '分類',
@@ -379,6 +379,7 @@ const PERSONAL_ES: PersonalPricingCopy = {
creatorDeveloperSupport: 'Soporte para creadores / desarrolladores',
popularModels: 'Modelos populares',
flagshipModels: 'Modelos insignia',
flagshipModelCount: '{count}+ modelos insignia',
imageModels: 'Modelos de imagen',
modelCount: '{count} modelos',
unlimited: 'Ilimitado',
@@ -396,7 +397,6 @@ const PERSONAL_ES: PersonalPricingCopy = {
estimatedRequests: 'Solicitudes estimadas cada 5 horas',
estimatedRequestsAria: 'Solicitudes de modelo estimadas cada 5 horas',
usageAllowanceNote: 'Los modelos populares incluyen una capacidad amplia, equivalente aproximadamente a $16 cada 5 horas · $40 por semana · $80 al mes. El uso real varía según el modelo y la longitud del contexto.',
usagePeakNote: 'Las estimaciones de DeepSeek V4 Flash / Pro usan precios fuera de hora punta; el consumo por unidad se duplica en hora punta (hora de Pekín 09:00–12:00 y 14:00–18:00).',
usageEstimatesNote: 'Las solicitudes estimadas pueden cambiar según el precio del modelo, los patrones de uso y los comentarios de los usuarios.',
comparisonTitle: 'Compara todos los modelos entre planes',
category: 'Categoría',
@@ -424,6 +424,7 @@ const PERSONAL_PT_BR: PersonalPricingCopy = {
creatorDeveloperSupport: 'Suporte para criadores / desenvolvedores',
popularModels: 'Modelos populares',
flagshipModels: 'Modelos de ponta',
flagshipModelCount: '{count}+ modelos de ponta',
imageModels: 'Modelos de imagem',
modelCount: '{count} modelos',
unlimited: 'Ilimitado',
@@ -441,7 +442,6 @@ const PERSONAL_PT_BR: PersonalPricingCopy = {
estimatedRequests: 'Solicitações estimadas a cada 5 horas',
estimatedRequestsAria: 'Solicitações de modelo estimadas a cada 5 horas',
usageAllowanceNote: 'Os modelos populares incluem uma franquia ampla, aproximadamente equivalente a $16 a cada 5 horas · $40 por semana · $80 por mês. O uso real varia conforme o modelo e o tamanho do contexto.',
usagePeakNote: 'As estimativas do DeepSeek V4 Flash / Pro usam preços fora do horário de pico; o consumo por unidade dobra no pico (horário de Pequim 09:00–12:00 e 14:00–18:00).',
usageEstimatesNote: 'As solicitações estimadas podem mudar conforme o preço do modelo, os padrões de uso e o feedback dos usuários.',
comparisonTitle: 'Compare todos os modelos entre os planos',
category: 'Categoria',
@@ -469,6 +469,7 @@ const PERSONAL_RU: PersonalPricingCopy = {
creatorDeveloperSupport: 'Поддержка авторов / разработчиков',
popularModels: 'Популярные модели',
flagshipModels: 'Флагманские модели',
flagshipModelCount: '{count}+ флагманских моделей',
imageModels: 'Модели изображений',
modelCount: '{count} моделей',
unlimited: 'Без ограничений',
@@ -486,7 +487,6 @@ const PERSONAL_RU: PersonalPricingCopy = {
estimatedRequests: 'Оценка запросов каждые 5 часов',
estimatedRequestsAria: 'Оценка запросов к моделям каждые 5 часов',
usageAllowanceNote: 'Популярные модели включают большой лимит, примерно равный $16 каждые 5 часов · $40 в неделю · $80 в месяц. Фактический расход зависит от модели и длины контекста.',
usagePeakNote: 'Оценки DeepSeek V4 Flash / Pro используют внепиковые цены; в часы пик (по пекинскому времени 09:00–12:00 и 14:00–18:00) расход единиц удваивается.',
usageEstimatesNote: 'Оценка числа запросов может меняться вместе с ценой модели, характером использования и отзывами пользователей.',
comparisonTitle: 'Сравните все модели во всех планах',
category: 'Категория',
@@ -514,6 +514,7 @@ const PERSONAL_FR: PersonalPricingCopy = {
creatorDeveloperSupport: 'Support créateur / développeur',
popularModels: 'Modèles populaires',
flagshipModels: 'Modèles phares',
flagshipModelCount: '{count}+ modèles phares',
imageModels: 'Modèles d’image',
modelCount: '{count} modèles',
unlimited: 'Illimité',
@@ -531,7 +532,6 @@ const PERSONAL_FR: PersonalPricingCopy = {
estimatedRequests: 'Requêtes estimées toutes les 5 heures',
estimatedRequestsAria: 'Requêtes de modèles estimées toutes les 5 heures',
usageAllowanceNote: 'Les modèles populaires incluent un quota généreux, soit environ 16 $ toutes les 5 heures · 40 $ par semaine · 80 $ par mois. L’usage réel varie selon le modèle et la longueur du contexte.',
usagePeakNote: 'Les estimations DeepSeek V4 Flash / Pro utilisent les tarifs hors pointe ; la consommation unitaire est doublée aux heures de pointe (heure de Pékin 09:00–12:00 et 14:00–18:00).',
usageEstimatesNote: 'Le nombre estimé de requêtes peut évoluer avec le prix des modèles, les usages et les retours des utilisateurs.',
comparisonTitle: 'Comparez tous les modèles selon les offres',
category: 'Catégorie',
@@ -559,6 +559,7 @@ const PERSONAL_KO: PersonalPricingCopy = {
creatorDeveloperSupport: '크리에이터 / 개발자 지원',
popularModels: '인기 모델',
flagshipModels: '플래그십 모델',
flagshipModelCount: '{count}+ 플래그십 모델',
imageModels: '이미지 모델',
modelCount: '모델 {count}개',
unlimited: '무제한',
@@ -576,7 +577,6 @@ const PERSONAL_KO: PersonalPricingCopy = {
estimatedRequests: '5시간마다 예상 사용 횟수',
estimatedRequestsAria: '5시간마다 예상 모델 요청 수',
usageAllowanceNote: '인기 모델에는 넉넉한 한도가 포함되며, 대략 5시간마다 $16 · 주당 $40 · 월 $80에 해당합니다. 실제 사용량은 모델과 컨텍스트 길이에 따라 달라집니다.',
usagePeakNote: 'DeepSeek V4 Flash / Pro 예상치는 비혼잡 시간대 요금을 기준으로 하며, 혼잡 시간대(베이징 시간 09:00–12:00, 14:00–18:00)에는 단위 사용량이 2배입니다.',
usageEstimatesNote: '모델별 예상 요청 수는 모델 가격, 사용 패턴, 사용자 피드백에 따라 조정될 수 있습니다.',
comparisonTitle: '요금제별 전체 모델 비교',
category: '분류',
@@ -604,6 +604,7 @@ const PERSONAL_DE: PersonalPricingCopy = {
creatorDeveloperSupport: 'Support für Kreative / Entwickler',
popularModels: 'Beliebte Modelle',
flagshipModels: 'Flaggschiffmodelle',
flagshipModelCount: '{count}+ Flaggschiffmodelle',
imageModels: 'Bildmodelle',
modelCount: '{count} Modelle',
unlimited: 'Unbegrenzt',
@@ -621,7 +622,6 @@ const PERSONAL_DE: PersonalPricingCopy = {
estimatedRequests: 'Geschätzte Anfragen alle 5 Stunden',
estimatedRequestsAria: 'Geschätzte Modellanfragen alle 5 Stunden',
usageAllowanceNote: 'Beliebte Modelle enthalten ein großzügiges Kontingent, ungefähr entsprechend $16 alle 5 Stunden · $40 pro Woche · $80 pro Monat. Die tatsächliche Nutzung variiert je nach Modell und Kontextlänge.',
usagePeakNote: 'Die Schätzungen für DeepSeek V4 Flash / Pro verwenden Nebenzeitenpreise; zu Spitzenzeiten (Peking-Zeit 09:00–12:00 und 14:00–18:00) zählt der Verbrauch doppelt.',
usageEstimatesNote: 'Die geschätzten Anfragen pro Modell können sich mit Modellpreisen, Nutzungsmustern und Nutzerfeedback ändern.',
comparisonTitle: 'Alle Modelle nach Tarif vergleichen',
category: 'Kategorie',
@@ -649,6 +649,7 @@ const PERSONAL_JA: PersonalPricingCopy = {
creatorDeveloperSupport: 'クリエイター / 開発者サポート',
popularModels: '人気モデル',
flagshipModels: 'フラッグシップモデル',
flagshipModelCount: 'フラッグシップモデル {count} 種以上',
imageModels: '画像モデル',
modelCount: '{count} モデル',
unlimited: '無制限',
@@ -666,7 +667,6 @@ const PERSONAL_JA: PersonalPricingCopy = {
estimatedRequests: '5 時間ごとの推定リクエスト数',
estimatedRequestsAria: '5 時間ごとのモデル推定リクエスト数',
usageAllowanceNote: '人気モデルには十分な利用枠が含まれ、目安は 5 時間ごとに $16 · 週 $40 · 月 $80 相当です。実際の使用量はモデルとコンテキスト長によって異なります。',
usagePeakNote: 'DeepSeek V4 Flash / Pro の推定値はオフピーク料金を使用します。ピーク時間(北京時間 09:00–12:00、14:00–18:00)は単位使用量が 2 倍になります。',
usageEstimatesNote: 'モデル別の推定回数は、モデル価格、利用状況、ユーザーフィードバックに応じて変更される場合があります。',
comparisonTitle: 'プラン別に全モデルを比較',
category: 'カテゴリー',
@@ -709,8 +709,8 @@ const EN: PricingContent = {
freeTrialCreditLabel: 'Limited trial model credits (valid for 7 days)',
firstMonthTag: '{pct}% off 1st month',
yearlyDiscountTag: '{pct}% off',
yearlySubline: 'Billed yearly · {totalUsd} / year (save {savingsUsd})',
monthlyRenewal: 'Then {amountUsd} / mo',
yearlySubline: 'Billed {totalUsd}/year',
monthlyRenewal: 'First-month price',
yearlySaveCta: 'Save {savingsUsd} yearly',
},
free: {
@@ -720,8 +720,8 @@ const EN: PricingContent = {
features: ['BYOK provider keys · Local coding agents', 'Community support'],
},
go: {
tagline: 'Lightweight needs, effortless delivery · Zero-config',
ctaLabel: 'Subscribe to Go',
tagline: 'Light needs · Easy delivery',
ctaLabel: 'Subscribe',
allowance: '8 popular models · ample allowance',
features: [
'Full design and coding capabilities',
@@ -731,8 +731,8 @@ const EN: PricingContent = {
},
plans: {
plus: {
tagline: 'Independent projects, solo delivery · Zero-config',
ctaLabel: 'Upgrade to Plus',
tagline: 'Everyday design · Continuous delivery',
ctaLabel: 'Subscribe',
concurrency: '2 concurrent tasks',
features: [
'Zero-config professional design agent',
@@ -742,8 +742,8 @@ const EN: PricingContent = {
],
},
pro: {
tagline: 'One person, a whole design team · Zero-config',
ctaLabel: 'Upgrade to Pro',
tagline: 'Complex projects · Efficient production',
ctaLabel: 'Subscribe',
concurrency: '5 concurrent tasks',
features: [
'Zero-config professional design agent',
@@ -753,8 +753,8 @@ const EN: PricingContent = {
],
},
max: {
tagline: 'Outsourced design costs, slashed · Zero-config',
ctaLabel: 'Upgrade to Max',
tagline: 'High-volume creation · Consistent output',
ctaLabel: 'Subscribe',
concurrency: '10 concurrent tasks',
features: [
'Zero-config professional design agent',
@@ -65,9 +65,9 @@ const EN: TeamPricingContent = {
increaseSeats: 'Increase seats',
minSeatsNote: 'Team plans require at least {count} seats.',
perSeatMonth: '/ seat / month',
monthlyTotal: 'First month only {amount}',
yearlyTotal: 'First year only {amount}',
checkout: 'Upgrade team',
monthlyTotal: '{amount} / month for your team',
yearlyTotal: '{amount} billed for the first year',
checkout: 'Upgrade team · {amount}/year',
teamFeatures: [
'Share and manage projects, design systems, and plugins as a team',
'Everyone can view and comment; only the project creator can edit',
@@ -109,9 +109,9 @@ const ZH: TeamPricingContent = {
increaseSeats: '增加席位',
minSeatsNote: '团队版最少需要 {count} 个席位。',
perSeatMonth: '/ 席 / 月',
monthlyTotal: '首月仅需 {amount}',
yearlyTotal: '首年仅需 {amount}',
checkout: '升级团队版',
monthlyTotal: '团队合计 {amount} / 月',
yearlyTotal: '首年应付 {amount}',
checkout: '升级团队版 · {amount}/年',
teamFeatures: [
'项目、设计系统与插件,团队统一共享与管理',
'成员均可查看和评论项目,仅项目创建者可以编辑',
@@ -153,9 +153,9 @@ const JA: TeamPricingContent = {
increaseSeats: '席数を増やす',
minSeatsNote: 'Team プランは最低 {count} 席から利用できます。',
perSeatMonth: '/ 席 / 月',
monthlyTotal: '初月は {amount} のみ',
yearlyTotal: '初年度は {amount} のみ',
checkout: 'Team にアップグレード',
monthlyTotal: 'チーム合計 月額 {amount}',
yearlyTotal: '初年度の請求額 {amount}',
checkout: 'Team にアップグレード · 年額 {amount}',
teamFeatures: [
'プロジェクト、Design Systems、プラグインをチームで共有・管理',
'全員が閲覧とコメント可能。編集はプロジェクト作成者のみ',
@@ -197,9 +197,9 @@ const KO: TeamPricingContent = {
increaseSeats: '좌석 늘리기',
minSeatsNote: 'Team 요금제는 최소 {count}개 좌석이 필요합니다.',
perSeatMonth: '/ 좌석 / 월',
monthlyTotal: '첫 달은 {amount}',
yearlyTotal: '첫해는 {amount}',
checkout: 'Team으로 업그레이드',
monthlyTotal: '팀 합계 월 {amount}',
yearlyTotal: '첫해 청구 금액 {amount}',
checkout: 'Team으로 업그레이드 · 연 {amount}',
teamFeatures: [
'프로젝트, Design Systems, 플러그인을 팀에서 공유하고 관리',
'모든 구성원이 보고 댓글을 달 수 있으며 편집은 프로젝트 생성자만 가능',
@@ -241,9 +241,9 @@ const DE: TeamPricingContent = {
increaseSeats: 'Sitz hinzufügen',
minSeatsNote: 'Team-Pläne erfordern mindestens {count} Sitze.',
perSeatMonth: '/ Sitz / Monat',
monthlyTotal: 'Im ersten Monat nur {amount}',
yearlyTotal: 'Im ersten Jahr nur {amount}',
checkout: 'Auf Team upgraden',
monthlyTotal: 'Teamgesamtpreis {amount} / Monat',
yearlyTotal: 'Im ersten Jahr werden {amount} berechnet',
checkout: 'Auf Team upgraden · {amount}/Jahr',
teamFeatures: [
'Projekte, Design Systems und Plugins gemeinsam teilen und verwalten',
'Alle können ansehen und kommentieren; nur Ersteller bearbeiten ihr Projekt',
@@ -285,9 +285,9 @@ const FR: TeamPricingContent = {
increaseSeats: 'Ajouter un siège',
minSeatsNote: 'Les offres Team nécessitent au moins {count} sièges.',
perSeatMonth: '/ siège / mois',
monthlyTotal: 'Premier mois à seulement {amount}',
yearlyTotal: 'Première année à seulement {amount}',
checkout: 'Passer à Team',
monthlyTotal: 'Total équipe : {amount} / mois',
yearlyTotal: '{amount} facturés la première année',
checkout: 'Passer à Team · {amount}/an',
teamFeatures: [
'Partager et gérer en équipe projets, Design Systems et plugins',
'Tous peuvent consulter et commenter ; seul le créateur du projet le modifie',
@@ -329,9 +329,9 @@ const RU: TeamPricingContent = {
increaseSeats: 'Увеличить число мест',
minSeatsNote: 'Для командного тарифа нужно минимум {count} места.',
perSeatMonth: '/ место / месяц',
monthlyTotal: 'Первый месяц — всего {amount}',
yearlyTotal: 'Первый год — всего {amount}',
checkout: 'Перейти на Team',
monthlyTotal: '{amount} / месяц для команды',
yearlyTotal: '{amount} за первый год',
checkout: 'Перейти на Team · {amount}/год',
teamFeatures: [
'Общие проекты, Design Systems и плагины с управлением для команды',
'Все могут смотреть и комментировать; редактирует только создатель проекта',
@@ -373,9 +373,9 @@ const ES: TeamPricingContent = {
increaseSeats: 'Añadir un asiento',
minSeatsNote: 'Los planes Team requieren al menos {count} asientos.',
perSeatMonth: '/ asiento / mes',
monthlyTotal: 'Primer mes por solo {amount}',
yearlyTotal: 'Primer año por solo {amount}',
checkout: 'Mejorar a Team',
monthlyTotal: '{amount} / mes para tu equipo',
yearlyTotal: '{amount} facturados el primer año',
checkout: 'Mejorar a Team · {amount}/año',
teamFeatures: [
'Compartir y gestionar proyectos, Design Systems y plugins en equipo',
'Todos pueden ver y comentar; solo el creador del proyecto puede editarlo',
@@ -417,9 +417,9 @@ const PT_BR: TeamPricingContent = {
increaseSeats: 'Aumentar assentos',
minSeatsNote: 'Os planos Team exigem pelo menos {count} assentos.',
perSeatMonth: '/ assento / mês',
monthlyTotal: 'Primeiro mês por apenas {amount}',
yearlyTotal: 'Primeiro ano por apenas {amount}',
checkout: 'Fazer upgrade para Team',
monthlyTotal: '{amount} / mês para sua equipe',
yearlyTotal: '{amount} cobrados no primeiro ano',
checkout: 'Fazer upgrade para Team · {amount}/ano',
teamFeatures: [
'Compartilhe e gerencie projetos, Design Systems e plugins em equipe',
'Todos podem ver e comentar; apenas o criador do projeto pode editar',
@@ -461,9 +461,9 @@ const IT: TeamPricingContent = {
increaseSeats: 'Aumenta le postazioni',
minSeatsNote: 'I piani Team richiedono almeno {count} postazioni.',
perSeatMonth: '/ postazione / mese',
monthlyTotal: 'Primo mese a soli {amount}',
yearlyTotal: 'Primo anno a soli {amount}',
checkout: 'Passa a Team',
monthlyTotal: '{amount} / mese per il tuo team',
yearlyTotal: '{amount} fatturati il primo anno',
checkout: 'Passa a Team · {amount}/anno',
teamFeatures: [
'Condividi e gestisci progetti, Design Systems e plugin come team',
'Tutti possono vedere e commentare; modifica solo chi crea il progetto',
@@ -505,9 +505,9 @@ const TR: TeamPricingContent = {
increaseSeats: 'Koltuk sayısını artır',
minSeatsNote: 'Team planları en az {count} koltuk gerektirir.',
perSeatMonth: '/ koltuk / ay',
monthlyTotal: 'İlk ay yalnızca {amount}',
yearlyTotal: 'İlk yıl yalnızca {amount}',
checkout: 'Team’e yükselt',
monthlyTotal: 'Ekibiniz için aylık {amount}',
yearlyTotal: 'İlk yıl için {amount} faturalandırılır',
checkout: 'Team’e yükselt · {amount}/yıl',
teamFeatures: [
'Projeleri, Design Systems öğelerini ve eklentileri ekipçe paylaşın ve yönetin',
'Herkes görüntüleyip yorum yapabilir; yalnızca proje sahibi düzenleyebilir',
+19
View File
@@ -116,6 +116,25 @@ export function cloudSubscribeUrl(
return url.toString();
}
/**
* Exact Team checkout handoff. Landing owns the comparison controls; Vela owns
* authentication, live catalog validation, workspace permissions, and Stripe.
* Carry the visitor's complete selection so Vela can prefill its existing
* confirmation dialog instead of asking for the same choices a second time.
*/
export function cloudTeamSubscribeUrl(
tier: TeamPlanTier,
interval: BillingInterval,
seats: number,
): string {
if (!Number.isSafeInteger(seats) || seats < 1) {
throw new RangeError('Team checkout seats must be a positive integer');
}
const url = new URL(cloudSubscribeUrl(tier, interval));
url.searchParams.set('seats', String(seats));
return url.toString();
}
/**
* Preserve an explicitly supplied workspace scope without inventing one from
* local state. The browser enhancement calls this same URL contract when the
+308 -106
View File
@@ -90,7 +90,8 @@ function teamView(tier: TeamPlanTierConfig, interval: 'monthly' | 'yearly') {
amount: usd(introSeatMonth),
strike: usd(regularSeatMonth),
discountPct,
intervalTotal: usd(intervalTotal),
monthlyTeamTotal: usd(intervalTotal / months).replace(/^\$/, ''),
intervalTotal: usd(intervalTotal).replace(/^\$/, ''),
};
}
@@ -270,9 +271,10 @@ const jsonLd = [
<div class="pr-team-grid">
<section class="pr-team-card" aria-labelledby="pr-team-title">
<svg class="pr-team-decoration" viewBox="0 0 24 24" aria-hidden="true">
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09Z" />
<path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.66 12.66 0 0 1 22 2c0 2.72-.78 7.5-6.05 11A22.35 22.35 0 0 1 12 15Z" />
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
<circle cx="9" cy="8" r="3" />
<path d="M3.5 19c.35-3.45 2.1-5.2 5.5-5.2s5.15 1.75 5.5 5.2" />
<circle cx="17.25" cy="9.25" r="2.25" />
<path d="M15.3 14.7c3.2-.75 5.05.68 5.2 3.8" />
</svg>
<h2 id="pr-team-title" class="pr-team-card-title">{teamContent.teamTitle}</h2>
<p class="pr-team-card-tagline">{teamContent.teamTagline}</p>
@@ -285,11 +287,18 @@ const jsonLd = [
{fillTemplate(L.yearlyDiscountTag, { pct: String(initialTeamView.discountPct) })}
</span>
</div>
<p class="pr-team-yearly-summary" data-team-summary>
{fillTemplate(teamContent.yearlyTotal, {
amount: initialTeamView.intervalTotal,
})}
</p>
<div class="pr-team-price-summary" aria-live="polite">
<p data-team-monthly-summary>
{fillTemplate(teamContent.monthlyTotal, {
amount: initialTeamView.monthlyTeamTotal,
})}
</p>
<strong data-team-yearly-summary>
{fillTemplate(teamContent.yearlyTotal, {
amount: initialTeamView.intervalTotal,
})}
</strong>
</div>
<div class="pr-team-controls">
<div class="pr-team-field">
@@ -355,15 +364,28 @@ const jsonLd = [
{fillTemplate(teamContent.minSeatsNote, { count: String(initialTeamTier.minSeats) })}
</p>
<div class="pr-team-model-offer" data-pricing-campaign-surface hidden>
<span class="pr-team-model-offer-copy">
<strong>{deepSeekCampaign.teamOfferTitle}</strong>
<small>{deepSeekCampaign.teamOfferModels}</small>
</span>
<span
class="pr-team-offer-countdown"
data-team-campaign-countdown
aria-label={deepSeekCampaign.windowLabel}
>
<b data-team-countdown-days data-day-unit={deepSeekCampaign.dayUnit}>
0{deepSeekCampaign.dayUnit}
</b>
<b data-team-countdown-hours>00</b>
<i aria-hidden="true">:</i>
<b data-team-countdown-minutes>00</b>
<i aria-hidden="true">:</i>
<b data-team-countdown-seconds>00</b>
</span>
</div>
<ul class="pr-team-feature-list">
{deepSeekCampaign.modelBenefits.map((benefit) => (
<li class="pr-campaign-model-benefit" data-pricing-campaign-surface hidden>
<div>
<strong>{benefit}</strong>
<small>{deepSeekCampaign.teamBenefitNote}</small>
</div>
</li>
))}
{teamContent.teamFeatures.map((feature) => <li>{feature}</li>)}
</ul>
@@ -372,14 +394,18 @@ const jsonLd = [
href={CLOUD_CONSOLE_URL}
data-pricing-cta
data-tier="team"
data-team-checkout-total
>
{teamContent.checkout}
{fillTemplate(teamContent.checkout, { amount: initialTeamView.intervalTotal })}
</a>
</section>
<section class="pr-team-card pr-enterprise-card" aria-labelledby="pr-enterprise-title">
<svg class="pr-team-decoration" viewBox="0 0 24 24" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.69 2.8a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.33 1.84.56 2.8.69A2 2 0 0 1 22 16.92Z" />
<path d="M5 21V6.5L12 3v18" />
<path d="M12 8h7v13" />
<path d="M3 21h18" />
<path d="M8 8.5h1M8 12h1M8 15.5h1M15 11h1M15 14h1" />
</svg>
<h2 id="pr-enterprise-title" class="pr-team-card-title">{teamContent.enterpriseTitle}</h2>
<p class="pr-team-card-tagline">{teamContent.enterpriseTagline}</p>
@@ -438,13 +464,34 @@ const jsonLd = [
const campaignSurfaces = root.querySelectorAll('[data-pricing-campaign-surface]');
const campaignStartAt = Date.parse(root.getAttribute('data-campaign-start-at') || '');
const campaignEndAt = Date.parse(root.getAttribute('data-campaign-end-at') || '');
const teamCampaignCountdown = root.querySelector('[data-team-campaign-countdown]');
const teamCountdownDays = root.querySelector('[data-team-countdown-days]');
const teamCountdownHours = root.querySelector('[data-team-countdown-hours]');
const teamCountdownMinutes = root.querySelector('[data-team-countdown-minutes]');
const teamCountdownSeconds = root.querySelector('[data-team-countdown-seconds]');
let campaignEligible = false;
let campaignVisible = false;
const updateTeamCampaignCountdown = (now) => {
if (!teamCampaignCountdown) return;
const remaining = Math.max(0, campaignEndAt - now);
const totalSeconds = Math.floor(remaining / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (teamCountdownDays) {
teamCountdownDays.textContent = `${days}${teamCountdownDays.getAttribute('data-day-unit') || ''}`;
}
if (teamCountdownHours) teamCountdownHours.textContent = String(hours).padStart(2, '0');
if (teamCountdownMinutes) teamCountdownMinutes.textContent = String(minutes).padStart(2, '0');
if (teamCountdownSeconds) teamCountdownSeconds.textContent = String(seconds).padStart(2, '0');
};
const updateCampaignVisibility = () => {
const now = Date.now();
campaignEligible = now >= campaignStartAt && now < campaignEndAt;
campaignVisible = campaignEligible;
campaignSurfaces.forEach((surface) => { surface.hidden = !campaignVisible; });
updateTeamCampaignCountdown(now);
return campaignVisible;
};
updateCampaignVisibility();
@@ -519,7 +566,28 @@ const jsonLd = [
continue;
}
if (tier === 'team') {
cta.setAttribute('href', scopedBillingPlanUrl());
const selectedTier = teamTiers.find(
(candidate) =>
candidate.tier ===
root.querySelector('[data-team-tier]')?.getAttribute('data-value'),
);
const selectedSeats = Number(
root.querySelector('[data-team-seats]')?.textContent || '',
);
if (!selectedTier || !Number.isSafeInteger(selectedSeats)) {
cta.setAttribute('href', scopedBillingPlanUrl());
continue;
}
const target = new URL(scopedBillingPlanUrl());
target.searchParams.set('billing', 'plan');
target.searchParams.set('plan', selectedTier.tier);
target.searchParams.set('interval', 'yearly');
target.searchParams.set(
'seats',
String(Math.max(selectedTier.minSeats, selectedSeats)),
);
target.searchParams.set('checkout', 'auto');
cta.setAttribute('href', target.toString());
continue;
}
const target = new URL(scopedBillingPlanUrl());
@@ -555,6 +623,8 @@ const jsonLd = [
};
const activateAudience = (audience, shouldTrackClick = true) => {
root.setAttribute('data-audience', audience);
const billingToggle = root.querySelector('.pr-toggle');
if (billingToggle) billingToggle.hidden = audience === 'team';
for (const button of root.querySelectorAll('[data-audience-btn]')) {
const active = button.getAttribute('data-audience-btn') === audience;
button.classList.toggle('is-active', active);
@@ -705,18 +775,18 @@ const jsonLd = [
if (teamSeatsOutput) teamSeatsOutput.textContent = String(teamSeats);
if (teamSeatsDec) teamSeatsDec.disabled = teamSeats <= tier.minSeats;
const interval = root.getAttribute('data-interval') === 'monthly' ? 'monthly' : 'yearly';
const interval = 'yearly';
const selected = tier[interval];
const months = interval === 'yearly' ? 12 : 1;
const introSeatMonth = selected.introPriceUsd / months;
const listSeatMonth = tier.monthly.priceUsd;
const discountPct = Math.round((1 - introSeatMonth / listSeatMonth) * 100);
const intervalTotal = selected.introPriceUsd * teamSeats;
const monthlyTeamTotal = intervalTotal / months;
const amount = usd(introSeatMonth);
const strike = usd(listSeatMonth);
const total = usd(intervalTotal);
const introTotalTemplate =
interval === 'yearly' ? teamCopy.yearlyTotal : teamCopy.monthlyTotal;
const monthlyTotal = usd(monthlyTeamTotal).replace(/^\$/, '');
const total = usd(intervalTotal).replace(/^\$/, '');
set('[data-audience-panel="team"]', '[data-team-amount]', amount);
set('[data-audience-panel="team"]', '[data-team-strike]', strike);
@@ -730,14 +800,25 @@ const jsonLd = [
);
set(
'[data-audience-panel="team"]',
'[data-team-summary]',
fillTeam(introTotalTemplate, { amount: total }),
'[data-team-monthly-summary]',
fillTeam(teamCopy.monthlyTotal, { amount: monthlyTotal }),
);
set(
'[data-audience-panel="team"]',
'[data-team-yearly-summary]',
fillTeam(teamCopy.yearlyTotal, { amount: total }),
);
set(
'[data-audience-panel="team"]',
'[data-team-checkout-total]',
fillTeam(teamCopy.checkout, { amount: total }),
);
set(
'[data-audience-panel="team"]',
'[data-team-min]',
fillTeam(teamCopy.minSeatsNote, { count: String(tier.minSeats) }),
);
syncCtas();
};
teamTierTrigger?.addEventListener('click', () => {
@@ -972,7 +1053,7 @@ const jsonLd = [
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: end;
width: min(1280px, 100%);
width: min(1200px, 100%);
min-height: 42px;
margin-top: 65px;
}
@@ -981,9 +1062,11 @@ const jsonLd = [
justify-self: end;
display: inline-flex;
align-items: center;
gap: 8px;
padding-bottom: 11px;
gap: 0;
min-height: 42px;
margin-right: 24px;
}
.pr-toggle[hidden] { display: none !important; }
.pr-audience-toggle {
grid-column: 2;
position: relative;
@@ -991,33 +1074,36 @@ const jsonLd = [
justify-content: center;
align-items: center;
gap: 10px;
width: 420px;
}
.pr-toggle-btn {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 36px;
border: 0;
background: transparent;
cursor: pointer;
font: inherit;
font-size: 0.9rem;
color: var(--ink-soft, #555);
padding: 4px 2px;
font-family: Albert Sans, Arial, sans-serif;
font-size: 13px;
font-weight: 600;
line-height: 1;
color: #858980;
padding: 0 2px;
transition: color 200ms cubic-bezier(0.23, 1, 0.32, 1);
}
.pr-toggle-btn.is-active { color: var(--ink); font-weight: 700; }
.pr-toggle-separator { color: var(--ink-soft, #aaa); }
.pr-toggle-btn.is-active { color: #151712; }
.pr-toggle-separator { margin: 0 9px; color: #c0c3bb; font-size: 14px; }
.pr-audience-btn {
position: relative;
min-width: 60px;
min-height: 38px;
padding: 0 16px;
border: 0;
border-radius: 8px;
border-radius: 9px;
background: transparent;
color: var(--ink-soft, #777);
font: inherit;
font-size: 12px;
color: #74786f;
font-family: Albert Sans, Arial, sans-serif;
font-size: 14px;
font-weight: 600;
line-height: 1;
white-space: nowrap;
@@ -1025,9 +1111,8 @@ const jsonLd = [
transition: color 200ms cubic-bezier(0.23, 1, 0.32, 1), background-color 200ms cubic-bezier(0.23, 1, 0.32, 1);
}
.pr-audience-btn.is-active {
color: var(--ink);
font-weight: 650;
background: color-mix(in srgb, var(--ink) 7%, transparent);
color: #11120f;
background: #e8e9e3;
}
.pr-toggle-save {
margin-left: 7px;
@@ -1043,7 +1128,8 @@ const jsonLd = [
.pr-controls-row { grid-template-columns: 1fr; gap: 14px; }
.pr-audience-toggle,
.pr-toggle { grid-column: 1; justify-self: center; }
.pr-toggle { padding-bottom: 0; }
.pr-audience-toggle { width: 100%; }
.pr-toggle { margin-right: 0; }
}
/* ---- Interval-conditional visibility ---- */
@@ -1053,14 +1139,17 @@ const jsonLd = [
/* ---- Shared hosted multimodal benefit ---- */
.pr-multimodal {
position: relative;
left: 50%;
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(420px, 1fr);
gap: 36px;
align-items: center;
max-width: 1280px;
width: min(1160px, calc(100vw - 48px));
max-width: none;
margin: 28px auto 0;
padding: 30px 34px;
overflow: hidden;
transform: translateX(-50%);
border: 1px solid var(--border, rgba(0, 0, 0, 0.12));
border-radius: 18px;
background:
@@ -1188,13 +1277,14 @@ const jsonLd = [
/* ---- Team / Enterprise plans ---- */
.pr-team-panel {
max-width: 900px;
margin: 40px auto 0;
width: min(1200px, 100%);
max-width: none;
margin: 23px auto 0;
}
.pr-team-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
gap: 12px;
align-items: stretch;
}
.pr-team-card {
@@ -1202,91 +1292,127 @@ const jsonLd = [
display: flex;
flex-direction: column;
min-width: 0;
padding: 28px;
min-height: 590px;
padding: 24px 20px 22px;
border: 0;
border-radius: 12px;
background: #fff;
overflow: hidden;
border: 1px solid var(--border, rgba(0, 0, 0, 0.12));
border-radius: 18px;
background: var(--surface, rgba(255, 255, 255, 0.62));
animation: pr-team-card-enter 0.65s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.pr-team-card:nth-child(2) { animation-delay: 0.09s; }
@keyframes pr-team-card-enter {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.pr-team-decoration {
position: absolute;
top: 20px;
right: 24px;
width: 58px;
height: 58px;
color: color-mix(in srgb, var(--brand, #2bd200) 20%, transparent);
right: 20px;
width: 60px;
height: 60px;
color: #55c94a;
opacity: .2;
fill: none;
stroke: currentColor;
stroke-width: 1.4;
stroke-width: 1.25;
stroke-linecap: round;
stroke-linejoin: round;
}
.pr-team-card-title {
position: relative;
margin: 0;
font-size: 1.2rem;
margin: 0 0 8px;
padding-right: 76px;
color: #242622;
font-size: 20px;
font-weight: 700;
color: var(--ink);
line-height: 1.1;
letter-spacing: -.025em;
}
.pr-team-card-tagline {
position: relative;
margin: 5px 0 0;
color: var(--ink-soft, #777);
font-size: 0.86rem;
min-height: 42px;
margin: 0;
padding-right: 76px;
color: var(--pricing-muted, #676c64);
font-size: 12px;
line-height: 1.45;
}
.pr-team-price-row {
display: flex;
min-height: 72px;
align-items: baseline;
align-content: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 28px;
gap: 6px 8px;
margin: 5px 0 0;
color: #2c2e2a;
}
.pr-team-amount {
font-family: var(--serif, Georgia, serif);
font-size: 2.2rem;
font-family: Albert Sans, Arial, sans-serif;
font-size: 50px;
line-height: 1;
letter-spacing: -.045em;
}
.pr-team-strike { color: #858981; font-size: 16px; text-decoration-thickness: 1.5px; }
.pr-team-per { color: #73776f; font-size: 12px; }
.pr-team-discount {
padding: 4px 7px;
border-radius: 999px;
background: #dff3d9;
color: #208a19;
font-size: 10px;
font-weight: 700;
line-height: 1;
}
.pr-team-strike { color: var(--ink-soft, #aaa); }
.pr-team-per { color: var(--ink-soft, #777); font-size: 0.85rem; }
.pr-team-discount {
padding: 0.16rem 0.5rem;
border-radius: 999px;
background: color-mix(in srgb, var(--brand, #2bd200) 18%, transparent);
color: var(--olive, #218c00);
font-size: 0.72rem;
font-weight: 700;
}
.pr-team-yearly-summary,
.pr-team-price-summary,
.pr-team-min-seats {
margin: 8px 0 0;
color: var(--ink-soft, #777);
font-size: 0.78rem;
color: var(--pricing-muted, #676c64);
font-weight: 400;
line-height: 1.35;
}
.pr-team-price-summary {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
min-height: 38px;
margin: 2px 0 0;
padding-bottom: 10px;
font-size: 12px;
}
.pr-team-price-summary p,
.pr-team-price-summary strong {
margin: 0;
color: inherit;
font: inherit;
}
.pr-team-controls {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
gap: 12px;
margin-top: 24px;
grid-template-columns: minmax(0, 1.35fr) minmax(150px, .9fr);
gap: 10px;
margin-top: 18px;
}
.pr-team-field {
display: flex;
min-width: 0;
flex-direction: column;
gap: 8px;
color: var(--ink-soft, #666);
font-size: 0.8rem;
font-weight: 600;
color: #454842;
font-size: 10px;
font-weight: 700;
line-height: 1.3;
}
.pr-team-select-trigger,
.pr-team-stepper {
box-sizing: border-box;
width: 100%;
min-height: 42px;
border: 1px solid var(--border, rgba(0, 0, 0, 0.14));
height: 44px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--ink);
font: inherit;
background: #f3f4f0;
color: #2e302c;
font: 400 13px/1 Albert Sans, Arial, sans-serif;
}
.pr-team-select {
position: relative;
@@ -1351,31 +1477,88 @@ const jsonLd = [
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 4px 6px;
padding: 0 6px;
}
.pr-team-stepper button {
border: 0;
background: transparent;
color: var(--ink);
cursor: pointer;
font-size: 1.2rem;
width: 30px;
height: 30px;
font-size: 20px;
}
.pr-team-stepper button:disabled { cursor: not-allowed; opacity: 0.35; }
.pr-team-stepper output { min-width: 2ch; text-align: center; color: var(--ink); }
.pr-team-stepper output { min-width: 2ch; text-align: center; color: #2e302c; font-size: 13px; font-weight: 400; }
.pr-team-min-seats { margin: 9px 0 0; font-size: 9px; }
.pr-team-model-offer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 68px;
margin-top: 18px;
padding: 12px 14px;
border-radius: 8px;
background: #eef5e9;
}
.pr-team-model-offer-copy {
display: block;
min-width: 0;
}
.pr-team-model-offer-copy strong {
display: block;
color: #292b27;
font-size: 11px;
font-weight: 700;
line-height: 1.35;
}
.pr-team-model-offer-copy small {
display: block;
margin-top: 3px;
color: #565a53;
font-size: 9px;
font-weight: 400;
line-height: 1.35;
}
.pr-team-offer-countdown {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 5px;
}
.pr-team-offer-countdown b {
display: grid;
min-width: 34px;
height: 38px;
place-items: center;
padding: 0 7px;
border-radius: 8px;
background: #dcf2d4;
color: #188c10;
font-size: 14px;
font-weight: 700;
line-height: 1.2;
}
.pr-team-offer-countdown i {
color: #188c10;
font-size: 12px;
font-style: normal;
}
.pr-team-feature-list {
list-style: none;
margin: 20px 0 0;
margin: 18px 0 22px;
padding: 0;
display: flex;
flex-direction: column;
gap: 11px;
gap: 14px;
}
.pr-team-feature-list li {
position: relative;
padding-left: 22px;
font-size: 0.86rem;
padding-left: 23px;
font-size: 12px;
line-height: 1.45;
color: var(--ink, #2a2a2a);
color: #30322e;
}
.pr-team-feature-list li::before {
content: '✓';
@@ -1383,7 +1566,9 @@ const jsonLd = [
left: 0;
top: 0;
font-weight: 700;
color: var(--olive, #218c00);
color: #188c10;
font-size: 14px;
line-height: 1.2;
}
/* The campaign row extends 8px outside the list. Match that offset with a
more specific rule so its checkmark stays on the exact same x-axis as
@@ -1395,22 +1580,39 @@ const jsonLd = [
transform: translateY(-50%);
}
.pr-team-checkout {
width: 100%;
width: calc(100% - 2px);
min-height: 42px;
margin-top: auto;
padding-top: 11px;
padding-bottom: 11px;
padding: 0;
border-radius: 8px;
background: #10110f;
color: #fff;
justify-content: center;
text-align: center;
font-size: 13px;
font-weight: 700;
font-family: Inter, PingFang SC, Microsoft YaHei, Arial, sans-serif;
line-height: normal;
}
.pr-team-feature-list + .pr-team-checkout { margin-top: 28px; }
@media (max-width: 820px) {
.pr-team-feature-list + .pr-team-checkout { margin-top: auto; }
.pr-enterprise-card .pr-team-feature-list { gap: 17px; margin-top: 22px; }
@media (max-width: 980px) {
.pr-team-panel { max-width: 760px; }
.pr-team-grid { grid-template-columns: 1fr; }
.pr-team-card { min-height: 0; }
}
@media (max-width: 520px) {
.pr-team-card { padding: 22px; }
.pr-team-controls { grid-template-columns: 1fr; }
.pr-team-model-offer { align-items: flex-start; flex-direction: column; }
.pr-audience-btn { min-width: 0; flex: 1; }
}
@media (prefers-reduced-motion: reduce) {
.pr-team-card {
animation: none;
transform: none;
}
}
/* ---- FAQ ---- */
.pr-faq { max-width: 760px; margin: 72px auto 0; }
+3
View File
@@ -173,6 +173,9 @@ export default defineConfig({
srcDir: './app',
outDir: './out',
trailingSlash: 'always',
// The Astro audit toolbar observes the live countdown and re-audits every
// image on each tick, creating a dev-only asset request storm.
devToolbar: { enabled: false },
vite: {
define: {
// Staging / PR-preview builds set OD_LANDING_NOINDEX=1. SeoHead reads
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="m4.709 15.955 4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 0 1-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="m9.27 15.29 7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 0 0-1.829-1A8.975 8.975 0 0 0 5.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"/></svg>

After

Width:  |  Height:  |  Size: 697 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Jimeng</title><path d="M5.257 1.808C8.768 6.74 14.431 9.45 20.064 8.403a7.398 7.398 0 0 0 1.281-.38c.855-.33 1.975 1.01 1.326 1.658-.43.428-.84.82-1.208 1.147a51.149 51.149 0 0 1-8.389 6.044 51.146 51.146 0 0 1-9.49 4.263c-.453.15-.98.304-1.546.457-.885.238-1.484-1.4-.77-1.975.378-.306.713-.617.978-.927C5.969 14.336 6.299 8.174 3.717 2.697a18.2 18.2 0 0 1-.364-.84C2.995.98 4.042.335 4.656 1.055c.243.284.453.545.601.753zM19.53 13.695c-2.324 2.863-2.816 5.944-2.378 8.583l.024.16c.117.79-.816 1.277-1.448.788a12.813 12.813 0 0 0-.093-.072c-2.12-1.614-4.62-2.828-7.948-2.446a27.95 27.95 0 0 0 6.086-2.627c2.21-1.275 4.147-2.774 5.756-4.386z"/></svg>

After

Width:  |  Height:  |  Size: 758 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Kimi</title><path d="M21.846 0a1.923 1.923 0 1 1 0 3.846H20.15a.226.226 0 0 1-.227-.226V1.923C19.923.861 20.784 0 21.846 0z"/><path d="m11.065 11.199 7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 0 0-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23h-2.69c-.103 0-.186.103-.186.23v15.95c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 0 1 .205-.023l6.484 4.772a7.677 7.677 0 0 0 3.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 0 1-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z"/></svg>

After

Width:  |  Height:  |  Size: 727 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>MiniMax</title><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 0 0 .744.709.74.74 0 0 0 .743-.709V9.099a2.06 2.06 0 0 1 2.071-2.049A2.06 2.06 0 0 1 24 9.1v6.561a.649.649 0 0 1-.652.645.649.649 0 0 1-.653-.645V9.1a.762.762 0 0 0-.766-.758.762.762 0 0 0-.766.758v7.472a2.037 2.037 0 0 1-2.048 2.026 2.037 2.037 0 0 1-2.048-2.026v-12.5a.785.785 0 0 0-.788-.753.785.785 0 0 0-.789.752l-.001 15.904A2.037 2.037 0 0 1 13.441 22a2.037 2.037 0 0 1-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 0 0 .372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 0 1-.652.645.648.648 0 0 1-.652-.645V4.07a.785.785 0 0 0-.789-.78.785.785 0 0 0-.789.78v14.013a2.06 2.06 0 0 1-2.07 2.048 2.06 2.06 0 0 1-2.071-2.048V9.1a.762.762 0 0 0-.766-.758.762.762 0 0 0-.766.758v3.8a2.06 2.06 0 0 1-2.071 2.049A2.06 2.06 0 0 1 0 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 0 1 2.07-2.048 2.06 2.06 0 0 1 2.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>NanoBanana</title><path d="M8.342 13.16H3.455c-.513 0-.772-.639-.408-1.012l1.608-1.653a3.166 3.166 0 0 1 4.565 0l.513.527.006-.015.038.017c.735-1.93.786-2.809.783-5.007v-.275c-.01-1.782-.02-3.935 1.965-4.22 2.603-.375 4.504 4.219 4.815 8.299a3.166 3.166 0 0 1 3.38.774l1.609 1.653c.365.375.106 1.012-.407 1.012H19.27c.072.264.11.542.11.828v5.664c0 .914-.994 1.292-1.602.576-.229-.27-1.067-1.25-2.155-2.52-2.92 4.183-10.266 6.462-12.34 3.006a.915.915 0 0 1-.05 0H2.49a.991.991 0 1 1 0-1.982h.246c.014-1.687 1.23-3.148 2.846-3.783a7.448 7.448 0 0 0 2.76-1.889zm7.543-2.145c0-.127-.001-.256-.005-.388a15.693 15.693 0 0 0-.632-3.939c-.38-1.282-.887-2.33-1.425-2.992-.545-.671-.906-.715-1.085-.69-.223.032-.292.098-.322.129-.05.052-.135.176-.209.45-.152.567-.15 1.286-.147 2.186v.244c.002 1.134-.009 2.028-.145 2.92a11.292 11.292 0 0 1-.537 2.08h4.507zm-3.468 3.056c-1.636 3.166-4.981 4.71-8.118 4.87a.562.562 0 0 0 .057 1.124c3.294-.169 6.921-1.749 8.845-5.081l-.784-.913z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1 -1
View File
@@ -62,7 +62,7 @@ test('Go banner keeps its sticky height in flow without double-offsetting the mo
});
test('Go banner classifies signed-out and unpaid visitors during the fixed window', () => {
assert.match(banner, /2026-08-20T20:00:00\+08:00/);
assert.match(banner, /2026-08-20T17:00:00\+08:00/);
assert.match(banner, /2026-09-03T20:00:00\+08:00/);
assert.match(banner, /\/api\/auth\/get-session/);
assert.match(banner, /if \(!session\?\.user\) \{\s*show\(\)/);
+336 -75
View File
@@ -8,6 +8,7 @@ import {
PLANS_JSON_URL,
PRICING_SNAPSHOT,
cloudSubscribeUrl,
cloudTeamSubscribeUrl,
formatUsd,
scopedBillingPlanUrl,
teamIntroTotalUsd,
@@ -35,6 +36,10 @@ const PRICING_INDIVIDUAL_PATH = new URL(
"../app/_components/pricing-individual-plans.astro",
import.meta.url,
);
const PRICING_CONTENT_PATH = new URL(
"../app/_lib/pricing-content.ts",
import.meta.url,
);
const GO_PLAN_MIMO_LOGO_PATH = new URL(
"../public/pricing-e-final/assets/mimo-logo-user-CWOWEwG5.png",
import.meta.url,
@@ -44,14 +49,14 @@ const GO_PLAN_ZHIPU_LOGO_PATH = new URL(
import.meta.url,
);
const FIRST_PARTY_MODEL_ICON_PATHS = [
"../public/agents/anthropic.svg",
"../public/agents/deepseek.svg",
"../public/agents/gemini.svg",
"../public/agents/minimax.svg",
"../public/agents/moonshot.svg",
"../public/agents/openai.svg",
"../public/agents/xai.svg",
"../public/model-icons/bytedance.svg",
"../public/model-icons/claude.svg",
"../public/model-icons/grok.svg",
"../public/model-icons/jimeng.svg",
"../public/model-icons/kimi.svg",
"../public/model-icons/minimax.svg",
"../public/model-icons/nanobanana.svg",
].map((path) => new URL(path, import.meta.url));
const CAMPAIGN_PATH = new URL(
"../app/_lib/pricing-campaign-content.ts",
@@ -159,6 +164,101 @@ describe("pricing contract", () => {
assert.doesNotMatch(plans, /--usage-label/);
});
it("matches the demo's individual taglines and compact billing copy", async () => {
const content = getPricingContent("en");
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
assert.deepEqual(
[
content.go.tagline,
content.plans.plus.tagline,
content.plans.pro.tagline,
content.plans.max.tagline,
],
[
"Light needs · Easy delivery",
"Everyday design · Continuous delivery",
"Complex projects · Efficient production",
"High-volume creation · Consistent output",
],
);
assert.equal(content.labels.monthlyRenewal, "First-month price");
assert.equal(content.labels.yearlySubline, "Billed {totalUsd}/year");
assert.deepEqual(
[
content.go.ctaLabel,
content.plans.plus.ctaLabel,
content.plans.pro.ctaLabel,
content.plans.max.ctaLabel,
],
["Subscribe", "Subscribe", "Subscribe", "Subscribe"],
);
assert.match(
individualPlans,
/<span>\{tierCopy\[tier\]\.ctaLabel\}<\/span>/,
);
assert.doesNotMatch(individualPlans, /ctaLabel\} · \{L\.(?:monthly|yearly)\}/);
assert.match(
individualPlans,
/const compactSavings = \(amountUsd: number, interval: 'monthly' \| 'yearly'\)/,
);
assert.match(
individualPlans,
/locale === 'en' && interval === 'monthly' \? `\$\{amount\}\/mo` : amount/,
);
assert.match(
individualPlans,
/const compactBillingTotal = \(amountUsd: number\) =>\s*locale === 'en'/,
);
});
it("keeps recommendation ribbons legible, animated, and motion-safe", async () => {
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
assert.match(
individualPlans,
/\.new-plan-ribbon\s*\{[^}]*font-size:\s*11px;/s,
);
assert.match(
individualPlans,
/\.new-plan-ribbon span\s*\{[^}]*background:\s*linear-gradient\(110deg,[^}]*animation:\s*pricing-ribbon-text-shimmer 4\.2s linear infinite;/s,
);
assert.match(
individualPlans,
/\.new-plan-ribbon span\s*\{[^}]*-webkit-text-fill-color:\s*transparent;[^}]*background-clip:\s*text;/s,
);
assert.doesNotMatch(individualPlans, /\.new-plan-ribbon::after/);
assert.match(
individualPlans,
/@keyframes pricing-ribbon-text-shimmer\s*\{[^}]*background-position:\s*100% 0;[^}]*\}[^}]*background-position:\s*-100% 0;/s,
);
assert.match(
individualPlans,
/@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\.new-plan-ribbon span\s*\{[^}]*-webkit-text-fill-color:\s*#173111;[^}]*animation:\s*none;/,
);
});
it("lets localized plan and billing copy wrap inside each pricing card", async () => {
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
assert.match(
individualPlans,
/\.plan-top\s*\{[^}]*min-height:\s*78px;/s,
);
assert.match(
individualPlans,
/\.plan-top p\s*\{[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s,
);
assert.match(
individualPlans,
/\.compact-price-detail\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*auto minmax\(0, 1fr\);[^}]*min-height:\s*46px;/s,
);
assert.match(
individualPlans,
/\.compact-price-detail > small\s*\{[^}]*min-width:\s*0;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s,
);
});
it("serves the model comparison icons from first-party assets", async () => {
const [individualPlans, ...icons] = await Promise.all([
readFile(PRICING_INDIVIDUAL_PATH, "utf8"),
@@ -168,16 +268,26 @@ describe("pricing contract", () => {
for (const icon of icons) assert.ok(icon.byteLength > 0);
assert.doesNotMatch(individualPlans, /unpkg\.com|@latest/);
for (const asset of [
"/agents/anthropic.svg",
"/agents/deepseek.svg",
"/agents/openai.svg",
"/model-icons/claude.svg",
"/model-icons/grok.svg",
"/model-icons/jimeng.svg",
"/model-icons/kimi.svg",
"/model-icons/minimax.svg",
"/model-icons/nanobanana.svg",
]) {
assert.ok(individualPlans.includes(asset), asset);
}
for (const superseded of [
"/agents/anthropic.svg",
"/agents/gemini.svg",
"/agents/minimax.svg",
"/agents/moonshot.svg",
"/agents/openai.svg",
"/agents/xai.svg",
"/model-icons/bytedance.svg",
]) {
assert.ok(individualPlans.includes(asset), asset);
assert.equal(individualPlans.includes(superseded), false, superseded);
}
});
@@ -258,6 +368,14 @@ describe("pricing contract", () => {
individualPlans,
/const popularStatus = popularAccessStatus\(model\.name, tier\);/,
);
assert.match(
individualPlans,
/\{popularStatus\.kind === 'unlimited' && <em class=\{popularStatus\.kind\}>\{popularStatus\.text\}<\/em>\}/,
);
assert.doesNotMatch(
individualPlans,
/^\s*<em class=\{popularStatus\.kind\}>\{popularStatus\.text\}<\/em>$/m,
);
assert.match(
individualPlans,
/return popularAccessStatus\(modelName, tier\);/,
@@ -268,6 +386,44 @@ describe("pricing contract", () => {
);
});
it("leads the comparison with Go unlimited models and keeps ample access check-only", async () => {
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
const comparisonBlock = individualPlans.match(
/const comparisonPopular = \[([\s\S]*?)\]\.map/,
)?.[1];
assert.ok(comparisonBlock);
assert.deepEqual(
Array.from(comparisonBlock.matchAll(/'([^']+)'/g), (match) => match[1]).slice(0, 3),
["DeepSeek V4 Flash", "DeepSeek V4 Pro", "GLM-5.2"],
);
assert.match(
individualPlans,
/status\.kind === 'more-ample'\s*\?\s*\(\s*<td><span class=\{`model-access-status \$\{status\.kind\}`\} aria-label=\{status\.text\}><i class="status-icon check"><\/i><\/span><\/td>/s,
);
});
it("renders paid flagship headings as one localized phrase", async () => {
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
for (const locale of PRICING_LOCALES) {
assert.match(
getPricingContent(locale).personal.flagshipModelCount,
/\{count\}/,
`${locale} must localize the complete flagship count heading`,
);
}
assert.equal(
getPricingContent("en").personal.flagshipModelCount,
"{count}+ flagship models",
);
assert.match(
individualPlans,
/tier === 'go' \? P\.flagshipModels : fillTemplate\(P\.flagshipModelCount, \{ count: String\(flagship\.length\) \}\)/,
);
assert.doesNotMatch(individualPlans, /\} · \$\{P\.flagshipModels\}/);
});
it("derives live Personal benefit totals from the catalog", async () => {
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
@@ -297,6 +453,15 @@ describe("pricing contract", () => {
);
});
it("omits the DeepSeek peak-pricing estimate note from Personal usage", async () => {
const individualPlans = await readFile(PRICING_INDIVIDUAL_PATH, "utf8");
const pricingContent = await readFile(PRICING_CONTENT_PATH, "utf8");
assert.doesNotMatch(individualPlans, /usagePeakNote/);
assert.doesNotMatch(pricingContent, /usagePeakNote/);
assert.doesNotMatch(pricingContent, /DeepSeek V4 Flash \/ Pro estimates use off-peak pricing/);
});
it("keeps the DeepSeek plan benefits without a Pricing campaign banner", async () => {
const page = await readFile(PRICING_PAGE_PATH, "utf8");
const campaign = await readFile(CAMPAIGN_PATH, "utf8");
@@ -389,20 +554,36 @@ describe("pricing contract", () => {
);
});
it("aligns the highlighted campaign checkmark with the Team benefit list", async () => {
const page = await readFile(PRICING_PAGE_PATH, "utf8");
it("renders one combined Team campaign offer with a live countdown", async () => {
const [page, campaign] = await Promise.all([
readFile(PRICING_PAGE_PATH, "utf8"),
readFile(CAMPAIGN_PATH, "utf8"),
]);
const teamPanel = page.slice(
page.indexOf('id="pr-team-panel"'),
page.indexOf('<p class="pr-foot"'),
);
assert.match(
page,
/\.pr-campaign-model-benefit > div\s*\{[\s\S]*display:\s*grid;[\s\S]*gap:\s*2px;/,
"the campaign date note must render on its own line below the model benefit",
);
assert.match(page, /\.pr-campaign-model-benefit::before\s*\{\s*left:\s*8px;\s*\}/);
assert.match(
page,
/\.pr-team-feature-list li\.pr-campaign-model-benefit::before\s*\{[\s\S]*left:\s*8px;[\s\S]*top:\s*18px;[\s\S]*transform:\s*translateY\(-50%\);/,
"the team campaign checkmark must override the later base list rule",
assert.match(campaign, /teamOfferTitle: 'Unlimited model access'/);
assert.match(campaign, /teamOfferModels: 'DeepSeek V4 Flash and V4 Pro'/);
assert.equal(
teamPanel.match(/data-pricing-campaign-surface/g)?.length,
1,
"Team must expose one combined activity surface",
);
assert.doesNotMatch(teamPanel, /modelBenefits\.map/);
assert.match(teamPanel, /class="pr-team-model-offer"/);
assert.match(teamPanel, /data-team-campaign-countdown/);
assert.match(teamPanel, /data-team-countdown-days/);
assert.match(teamPanel, /data-team-countdown-hours/);
assert.match(teamPanel, /data-team-countdown-minutes/);
assert.match(teamPanel, /data-team-countdown-seconds/);
assert.match(page, /Math\.max\(0, campaignEndAt - now\)/);
const offerRule = page.match(/\.pr-team-model-offer\s*\{([^}]*)\}/)?.[1];
assert.ok(offerRule);
assert.match(offerRule, /background:\s*#eef5e9;/);
assert.match(offerRule, /border-radius:\s*8px;/);
assert.match(offerRule, /padding:\s*12px 14px;/);
});
it("keeps the multimodal coming-soon note above the video label", async () => {
@@ -444,6 +625,11 @@ describe("pricing contract", () => {
1,
"the retained Cloud capability section must render exactly once",
);
assert.match(
page,
/\.pr-multimodal\s*\{[\s\S]*?left:\s*50%;[\s\S]*?width:\s*min\(1160px, calc\(100vw - 48px\)\);[\s\S]*?max-width:\s*none;[\s\S]*?transform:\s*translateX\(-50%\);/,
"the Cloud capability card must share the comparison table width",
);
});
it("points the public pricing URL at the landing-page JSON contract", () => {
@@ -459,6 +645,14 @@ describe("pricing contract", () => {
cloudSubscribeUrl("pro", "yearly"),
"https://open-design.ai/cloud/dashboard?billing=plan&plan=pro&interval=yearly&checkout=auto",
);
assert.equal(
cloudTeamSubscribeUrl("team_pro", "yearly", 4),
"https://open-design.ai/cloud/dashboard?billing=plan&plan=team_pro&interval=yearly&checkout=auto&seats=4",
);
assert.throws(
() => cloudTeamSubscribeUrl("team_pro", "yearly", 0),
/positive integer/,
);
assert.equal(
scopedBillingPlanUrl("workspace-a"),
"https://open-design.ai/cloud/dashboard?billing=plan&workspaceId=workspace-a",
@@ -593,55 +787,35 @@ describe("pricing contract", () => {
);
});
it("renders all 16 introductory Team totals for interval, tier, and seat changes", () => {
const expected = {
team_basic: {
monthly: { 3: "First month only $12", 4: "First month only $16" },
yearly: { 3: "First year only $126", 4: "First year only $168" },
},
team_plus: {
monthly: { 3: "First month only $60", 4: "First month only $80" },
yearly: { 3: "First year only $630", 4: "First year only $840" },
},
team_pro: {
monthly: {
3: "First month only $220.50",
4: "First month only $294",
},
yearly: {
3: "First year only $2,268",
4: "First year only $3,024",
},
},
team_max: {
monthly: { 3: "First month only $369", 4: "First month only $492" },
yearly: {
3: "First year only $3,622.83",
4: "First year only $4,830.44",
},
},
} as const;
it("renders the Team monthly total, first-year total, and priced checkout from the annual offer", async () => {
const page = await readFile(PRICING_PAGE_PATH, "utf8");
const english = TEAM_PRICING_CONTENT_BY_LOCALE.en;
for (const tier of PRICING_SNAPSHOT.teamTiers) {
for (const interval of ["monthly", "yearly"] as const) {
for (const seats of [3, 4] as const) {
const template =
interval === "monthly"
? english.monthlyTotal
: english.yearlyTotal;
const rendered = template.replace(
"{amount}",
formatUsd(teamIntroTotalUsd(tier, interval, seats)),
);
assert.equal(
rendered,
expected[tier.tier][interval][seats],
`${tier.tier} ${interval} ${seats} seats`,
);
}
for (const seats of [3, 4] as const) {
const annualTotal = teamIntroTotalUsd(tier, "yearly", seats);
const monthlyTotal = annualTotal / 12;
const amount = (value: number) => formatUsd(value).replace(/^\$/, "");
assert.equal(
english.monthlyTotal.replace("{amount}", amount(monthlyTotal)),
`${amount(monthlyTotal)} / month for your team`,
);
assert.equal(
english.yearlyTotal.replace("{amount}", amount(annualTotal)),
`${amount(annualTotal)} billed for the first year`,
);
assert.equal(
english.checkout.replace("{amount}", amount(annualTotal)),
`Upgrade team · ${amount(annualTotal)}/year`,
);
}
}
assert.match(page, /data-team-monthly-summary/);
assert.match(page, /data-team-yearly-summary/);
assert.match(page, /data-team-checkout-total/);
assert.match(page, /const interval = 'yearly';/);
});
it("removes the obsolete Team-coming-soon banner", async () => {
@@ -665,10 +839,32 @@ describe("pricing contract", () => {
// generic global section padding must be cancelled on the component root.
assert.match(individualPlans, /\.demo-individual-pricing\s*\{[^}]*padding:\s*0 !important;/s);
// Creator/Team and billing interval share one compact row above the cards.
// Creator/Team and billing interval share one compact row above the
// Individual cards. The billing group is right-aligned and disappears
// entirely when the Team tab is active, matching the approved demo.
assert.match(page, /class="pr-audience-toggle"[^>]*role="tablist"/);
assert.match(page, /\.pr-controls-row\s*\{[^}]*grid-template-columns:\s*1fr auto 1fr;/s);
assert.match(page, /\.pr-audience-btn\.is-active\s*\{[^}]*background:/s);
assert.match(page, /\.pr-toggle\s*\{[^}]*grid-column:\s*3;[^}]*justify-self:\s*end;/s);
assert.match(
page,
/\.pr-toggle-btn\s*\{[^}]*min-height:\s*36px;[^}]*font-size:\s*13px;[^}]*font-weight:\s*600;[^}]*line-height:\s*1;/s,
);
assert.match(
page,
/\.pr-audience-btn\s*\{[^}]*border-radius:\s*9px;[^}]*font-size:\s*14px;[^}]*font-weight:\s*600;/s,
);
assert.match(
page,
/\.pr-audience-btn\.is-active\s*\{[^}]*background:\s*#e8e9e3;/s,
);
assert.match(
page,
/const billingToggle = root\.querySelector\('\.pr-toggle'\);[\s\S]*?billingToggle\.hidden = audience === 'team';/,
);
assert.match(
page,
/\.pr-toggle\[hidden\]\s*\{\s*display:\s*none !important;\s*\}/,
);
// The visible Team tier control must never open the OS-native select popup.
assert.doesNotMatch(page, /<select[^>]*data-team-tier/);
@@ -681,6 +877,53 @@ describe("pricing contract", () => {
assert.doesNotMatch(page, /data-team-total/);
});
it("matches the approved Team card geometry and decoration artwork", async () => {
const page = await readFile(PRICING_PAGE_PATH, "utf8");
assert.match(
page,
/\.pr-team-panel\s*\{[^}]*width:\s*min\(1200px,\s*100%\);[^}]*max-width:\s*none;/s,
);
assert.match(page, /\.pr-team-grid\s*\{[^}]*gap:\s*12px;/s);
assert.match(
page,
/\.pr-team-card\s*\{[^}]*min-height:\s*590px;[^}]*padding:\s*24px 20px 22px;[^}]*border:\s*0;[^}]*border-radius:\s*12px;[^}]*background:\s*#fff;/s,
);
assert.match(
page,
/\.pr-team-decoration\s*\{[^}]*top:\s*20px;[^}]*right:\s*20px;[^}]*width:\s*60px;[^}]*height:\s*60px;[^}]*color:\s*#55c94a;[^}]*opacity:\s*\.2;/s,
);
assert.match(
page,
/<circle cx="9" cy="8" r="3"\s*\/>[\s\S]*?<path d="M3\.5 19c\.35-3\.45 2\.1-5\.2 5\.5-5\.2s5\.15 1\.75 5\.5 5\.2"\s*\/>/,
);
assert.match(
page,
/<path d="M5 21V6\.5L12 3v18"\s*\/>[\s\S]*?<path d="M12 8h7v13"\s*\/>[\s\S]*?<path d="M3 21h18"\s*\/>/,
);
});
it("reveals the Team cards with the demo's staggered entrance motion", async () => {
const page = await readFile(PRICING_PAGE_PATH, "utf8");
assert.match(
page,
/\.pr-team-card\s*\{[^}]*animation:\s*pr-team-card-enter 0\.65s cubic-bezier\(0\.16,\s*1,\s*0\.3,\s*1\) both;/s,
);
assert.match(
page,
/\.pr-team-card:nth-child\(2\)\s*\{\s*animation-delay:\s*0\.09s;\s*\}/,
);
assert.match(
page,
/@keyframes pr-team-card-enter\s*\{\s*from\s*\{\s*opacity:\s*0;\s*transform:\s*translateY\(12px\);\s*\}\s*to\s*\{\s*opacity:\s*1;\s*transform:\s*translateY\(0\);\s*\}\s*\}/,
);
assert.match(
page,
/@media \(prefers-reduced-motion:\s*reduce\)\s*\{[\s\S]*?\.pr-team-card\s*\{[^}]*animation:\s*none;[^}]*transform:\s*none;/,
);
});
it("localizes the flagship Pricing structure for every active locale", () => {
const activeLocales = LANDING_LOCALES.map((locale) => locale.code);
@@ -699,6 +942,7 @@ describe("pricing contract", () => {
);
assert.match(copy.monthlyTotal, /\{amount\}/);
assert.match(copy.yearlyTotal, /\{amount\}/);
assert.match(copy.checkout, /\{amount\}/);
assert.doesNotMatch(copy.monthlyTotal, /\{count\}|\{savings\}/);
assert.doesNotMatch(copy.yearlyTotal, /\{count\}|\{savings\}/);
assert.equal("yearlySummary" in copy, false);
@@ -706,23 +950,23 @@ describe("pricing contract", () => {
assert.equal(
TEAM_PRICING_CONTENT_BY_LOCALE.zh.monthlyTotal,
"首月仅需 {amount}",
"团队合计 {amount} / 月",
);
assert.equal(
TEAM_PRICING_CONTENT_BY_LOCALE.zh.yearlyTotal,
"首年仅需 {amount}",
"首年应付 {amount}",
);
assert.equal(
TEAM_PRICING_CONTENT_BY_LOCALE.en.monthlyTotal,
"First month only {amount}",
"{amount} / month for your team",
);
assert.equal(
TEAM_PRICING_CONTENT_BY_LOCALE.en.yearlyTotal,
"First year only {amount}",
"{amount} billed for the first year",
);
});
it("updates the Team intro-period total for period, tier, and seat controls", async () => {
it("updates the annual Team quote for tier and seat controls", async () => {
const page = await readFile(PRICING_PAGE_PATH, "utf8");
const updateStart = page.indexOf("const updateTeamPlan = () =>");
const updateEnd = page.indexOf(
@@ -735,17 +979,19 @@ describe("pricing contract", () => {
assert.match(
page,
/fillTemplate\(teamContent\.yearlyTotal,\s*\{\s*amount:\s*initialTeamView\.intervalTotal,\s*\}\)/s,
/fillTemplate\(teamContent\.monthlyTotal,\s*\{\s*amount:\s*initialTeamView\.monthlyTeamTotal,\s*\}\)/s,
);
assert.match(
updateTeamPlan,
/interval === 'yearly'\s*\?\s*teamCopy\.yearlyTotal\s*:\s*teamCopy\.monthlyTotal/,
/const interval = 'yearly';/,
);
assert.match(
updateTeamPlan,
/const intervalTotal = selected\.introPriceUsd \* teamSeats/,
);
assert.match(page, /const activateInterval = \(interval, via\) => \{[\s\S]*?updateTeamPlan\(\)/);
assert.match(updateTeamPlan, /fillTeam\(teamCopy\.monthlyTotal, \{ amount: monthlyTotal \}\)/);
assert.match(updateTeamPlan, /fillTeam\(teamCopy\.yearlyTotal, \{ amount: total \}\)/);
assert.match(updateTeamPlan, /fillTeam\(teamCopy\.checkout, \{ amount: total \}\)/);
assert.match(page, /const selectTeamTier = \(option\) => \{[\s\S]*?updateTeamPlan\(\)/);
assert.match(
page,
@@ -758,6 +1004,21 @@ describe("pricing contract", () => {
assert.doesNotMatch(updateTeamPlan, /teamCopy\.yearlySummary/);
assert.doesNotMatch(updateTeamPlan, /labels\.monthlyRenewal/);
assert.doesNotMatch(updateTeamPlan, /savings|regularTotal/);
assert.match(updateTeamPlan, /syncCtas\(\)/);
});
it("hands the exact Team selection to Vela checkout", async () => {
const page = await readFile(PRICING_PAGE_PATH, "utf8");
const syncStart = page.indexOf("const syncCtas = () =>");
const syncEnd = page.indexOf("syncCtas();", syncStart);
assert.notEqual(syncStart, -1);
assert.notEqual(syncEnd, -1);
const syncCtas = page.slice(syncStart, syncEnd);
assert.match(syncCtas, /target\.searchParams\.set\('plan', selectedTier\.tier\)/);
assert.match(syncCtas, /target\.searchParams\.set\('interval', 'yearly'\)/);
assert.match(syncCtas, /'seats',[\s\S]*String\(Math\.max\(selectedTier\.minSeats, selectedSeats\)\)/);
assert.match(syncCtas, /target\.searchParams\.set\('checkout', 'auto'\)/);
});
it("removes the superseded Team total and annual-savings copy", async () => {
+8
View File
@@ -5421,6 +5421,14 @@ function AppInner() {
? projectRouteWorkspaceContext.loading
: undefined
}
amrLoggedIn={amrLoginStatus?.loggedIn ?? null}
amrAccountPlan={
amrLoginStatus?.account?.plan?.trim()
|| amrLoginStatus?.user?.plan?.trim()
|| null
}
metricsConsent={config.telemetry?.metrics === true}
installationId={config.installationId}
/>
) : null}
{route.kind === 'project'
+10 -10
View File
@@ -18,18 +18,18 @@ export interface GoPlanCampaignCopy {
const english: GoPlanCampaignCopy = {
eyebrow: 'NEW PLAN · LAUNCH OFFER',
headline: 'Meet Go: more room to keep creating.',
description: 'A lighter plan for everyday design and coding, with popular models ready to use.',
headline: 'Low-cost design plan for everyone',
description: 'Professional design intelligence at a lower cost—so every idea moves faster from prompt to finished work.',
benefit: 'Go first month $5 · unlimited use',
status: 'UNLIMITED',
cta: 'View Go plan',
cta: 'View Go plan · Limited-time 50% off',
renewal: 'Then $10 / month',
boundary: 'Allowance details and offer terms are shown on Pricing.',
newBadge: 'NEW',
closeAria: 'Close dialog',
providersAria: 'Model providers available on Go',
workbenchBadge: 'Go first month $5 · unlimited use',
workbenchBadgeAria: 'Go first month $5, unlimited use — view Pricing',
workbenchBadge: 'The new Go Plan · ¥5 for the first month · Unlimited model usage',
workbenchBadgeAria: 'The new Go Plan, ¥5 for the first month, unlimited model usage — view Pricing',
};
const localized: Record<Locale, GoPlanCampaignCopy> = {
@@ -66,18 +66,18 @@ const localized: Record<Locale, GoPlanCampaignCopy> = {
},
'zh-CN': {
eyebrow: '全新套餐 · 首发特惠',
headline: 'Go,让灵感不断线。',
description: '面向日常设计与编码的轻量套餐,热门模型开箱即用。',
headline: '人人可用的低成本设计方案',
description: '以更低成本使用专业设计模型,让每一个想法更快成为作品。',
benefit: 'Go 首月 $5 · 无限用',
status: '无限使用',
cta: '查看 Go 套餐',
cta: '查看 Go 套餐 · 限时 5 折',
renewal: '之后 $10 / 月',
boundary: '具体额度与活动规则以 Pricing 页面为准。',
newBadge: 'NEW',
closeAria: '关闭弹窗',
providersAria: 'Go 套餐可用模型厂商',
workbenchBadge: 'Go 首月 $5 · 无限用',
workbenchBadgeAria: 'Go 首月 5 美元,无限用,查看 Pricing',
workbenchBadge: '全新 Go 套餐 · 首月 ¥5 · 模型无限用',
workbenchBadgeAria: '全新 Go 套餐,首月 ¥5,模型无限用,查看 Pricing',
},
'zh-TW': {
eyebrow: '全新方案 · 首發優惠',
+2 -2
View File
@@ -3,7 +3,7 @@ import type { DeepSeekV4FlashCampaignAudience } from './deepseek-v4-flash';
export const GO_PLAN_CAMPAIGN = {
id: 'go-plan-launch-2026',
window: {
startAt: '2026-08-20T20:00:00+08:00',
startAt: '2026-08-20T17:00:00+08:00',
endAtExclusive: '2026-09-03T20:00:00+08:00',
},
} as const;
@@ -24,7 +24,7 @@ export function goPlanCampaignNextBoundary(now: number): number | null {
return null;
}
/** Resolve subscription state without coupling the permanent Go entry to a campaign window. */
/** Resolve subscription state independently from the campaign's launch window. */
export function resolveSubscriptionAudience(input: {
plan: string | null | undefined;
loggedIn: boolean | null | undefined;
@@ -135,7 +135,7 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
padding: 54px 42px 28px;
padding: 64px 42px 28px 28px;
}
.goWelcomeCopy h2 {
@@ -251,7 +251,7 @@
width: 14px;
height: 14px;
object-fit: contain;
filter: invert(1) brightness(2);
filter: brightness(0) invert(1);
}
.goWelcomeBenefitZhipu img {
+52 -3
View File
@@ -74,6 +74,10 @@ import {
import { canUpgradeFromPlanTier, resolvePlanLabelTier } from '../collab/team-plan';
import { amrPlansUrlForProfile } from '../runtime/amr-guidance';
import { useWorkspaceInvalidation } from '../collab/workspace-events';
import { resolveDeepSeekV4FlashCampaignAudience } from '../campaigns/deepseek-v4-flash';
import { useDeepSeekV4FlashCampaignVisibility } from '../campaigns/use-deepseek-v4-flash-campaign';
import { resolveSubscriptionAudience } from '../campaigns/go-plan';
import { useGoPlanCampaignVisibility } from '../campaigns/use-go-plan-campaign';
import type { EntryHomeView } from '../router';
import type {
AccountMenuClickProps,
@@ -92,6 +96,7 @@ import {
stableAnalyticsErrorCode,
workspaceAnalyticsDimensions,
} from '../analytics/workspace';
import { WorkbenchCampaignBadge } from './WorkbenchCampaignBadge';
const REPO_URL = 'https://github.com/nexu-io/open-design';
const GITHUB_HELP_URL = `${REPO_URL}/issues/new`;
@@ -1078,6 +1083,10 @@ export function WorkspaceTopRightAccountCluster({
updaterSlot,
workspaceContextOverride,
workspaceContextLoading,
amrLoggedIn = null,
amrAccountPlan = null,
metricsConsent = false,
installationId,
}: {
onOpenSettings?: (section?: EntrySettingsSection) => void;
onSignedOut?: () => void | Promise<void>;
@@ -1085,29 +1094,69 @@ export function WorkspaceTopRightAccountCluster({
updaterSlot?: ReactNode;
workspaceContextOverride?: WorkspaceCollabContext | null;
workspaceContextLoading?: boolean;
amrLoggedIn?: boolean | null;
amrAccountPlan?: string | null;
metricsConsent?: boolean;
installationId?: string | null;
}) {
const ambient = useWorkspaceContext();
const hasExplicitWorkspaceContext = workspaceContextOverride !== undefined;
const context = hasExplicitWorkspaceContext
? workspaceContextOverride
: ambient.context;
const contextLoading = hasExplicitWorkspaceContext
? workspaceContextLoading === true
: ambient.loading;
const billingResponse = useWorkspaceBillingResponse({
context,
loading: hasExplicitWorkspaceContext
? workspaceContextLoading === true
: ambient.loading,
loading: contextLoading,
});
// Plan and money are both workspace-scoped questions, so both go through a
// context-partitioned projection — `response.summary` on its own is an
// ACCOUNT read (`workspaceId: null` by contract). Same rule as EntryShell.
const billing = workspaceBillingSummaryForContext(billingResponse, context);
const balanceUsd = workspaceBillingBalanceUsd(billingResponse, context);
const deepSeekCampaignVisibility = useDeepSeekV4FlashCampaignVisibility();
const goPlanCampaignVisibility = useGoPlanCampaignVisibility();
const campaignPlan = resolvePlanLabelTier({
billing,
context,
accountPlan:
contextLoading || context?.workspaceType === 'team'
? null
: amrAccountPlan,
});
const deepSeekCampaignAudience = resolveDeepSeekV4FlashCampaignAudience({
plan: campaignPlan,
loggedIn: amrLoggedIn,
now: deepSeekCampaignVisibility.now,
});
const subscriptionAudience = resolveSubscriptionAudience({
plan: campaignPlan,
loggedIn: amrLoggedIn,
});
const campaignKind =
subscriptionAudience === 'unpaid'
? goPlanCampaignVisibility.visible
? 'go'
: null
: deepSeekCampaignAudience === 'paid'
? 'deepseek'
: null;
return (
<EntryTopRightCluster
page="project"
context={context}
billing={billing}
balanceUsd={balanceUsd}
leadingSlot={campaignKind ? (
<WorkbenchCampaignBadge
kind={campaignKind}
page="project"
metricsConsent={metricsConsent}
installationId={installationId}
/>
) : null}
updaterSlot={updaterSlot}
onOpenSettings={onOpenSettings}
onSignedOut={onSignedOut}
+14 -82
View File
@@ -39,12 +39,9 @@ import {
trackOnboardingCompleteResult,
trackOnboardingRuntimeScanResult,
trackPageView,
trackDeepSeekCampaignBadgeClick,
trackDeepSeekCampaignBadgeSurfaceView,
} from '../analytics/events';
import {
amrHandoffDeviceId,
attributedAmrUrl,
recordAmrEntry,
type AmrEntryAttribution,
} from '../analytics/amr-attribution';
@@ -158,11 +155,10 @@ import { resolvePlanLabelTier } from '../collab/team-plan';
import { resolveDeepSeekV4FlashCampaignAudience } from '../campaigns/deepseek-v4-flash';
import { useDeepSeekV4FlashCampaignVisibility } from '../campaigns/use-deepseek-v4-flash-campaign';
import {
GO_PLAN_PRICING_URL,
resolveSubscriptionAudience,
} from '../campaigns/go-plan';
import { useGoPlanCampaignVisibility } from '../campaigns/use-go-plan-campaign';
import { getGoPlanCampaignCopy } from '../campaigns/go-plan-content';
import { WorkbenchCampaignBadge } from './WorkbenchCampaignBadge';
import {
beginWorkspaceScopedRead,
workspaceIdentityCacheKey,
@@ -611,8 +607,7 @@ export function EntryShell({
onAmrLoginStatusChange,
artifactUpgradeSlot,
}: Props) {
const { locale, t } = useI18n();
const goPlanCopy = getGoPlanCampaignCopy(locale);
const { t } = useI18n();
// Each entry sub-view (home / projects / design-systems) is its own
// URL now, so the browser back/forward buttons work and a deep link
// to /design-systems lands on that section. We derive the active
@@ -704,7 +699,9 @@ export function EntryShell({
: 'unknown';
const topRightCampaignKind =
subscriptionAudience === 'unpaid'
? 'go'
? goPlanCampaignVisibility.visible
? 'go'
: null
: deepSeekV4FlashCampaignAudience === 'paid'
? 'deepseek'
: null;
@@ -1189,62 +1186,6 @@ export function EntryShell({
scrollContainer.scrollTop = 0;
}, [view]);
const analytics = useAnalytics();
useEffect(() => {
if (view !== 'home' || !topRightCampaignKind) return;
const go = topRightCampaignKind === 'go';
if (go) return;
trackDeepSeekCampaignBadgeSurfaceView(analytics.track, {
page_name: 'home',
area: 'campaign_badge',
element: 'deepseek_v4_pro',
campaign_id: 'deepseek_v4_pro',
user_state: 'paid',
});
}, [analytics.track, topRightCampaignKind, view]);
const openCampaignPricing = useCallback(() => {
if (!topRightCampaignKind) return;
const go = topRightCampaignKind === 'go';
if (go) {
window.open(GO_PLAN_PRICING_URL, '_blank', 'noopener,noreferrer');
return;
}
trackDeepSeekCampaignBadgeClick(analytics.track, {
page_name: 'home',
area: 'campaign_badge',
element: 'open_pricing',
campaign_id: 'deepseek_v4_pro',
user_state: 'paid',
});
const attribution = recordAmrEntry(
analytics.track,
'deepseek_workbench_badge',
new Date(),
{
metricsConsent: config.telemetry?.metrics === true,
campaignId: 'deepseek_v4_pro',
conversionSource: 'deepseek_workbench_badge',
},
);
const deviceId = amrHandoffDeviceId({
metricsConsent: config.telemetry?.metrics === true,
resolvedDeviceId: getResolvedDeviceId(),
installationId: config.installationId,
});
// Both campaign badges are lightweight discovery entries. Pricing owns
// comparison; only a concrete Pricing card hands checkout to Cloud.
const destination = GO_PLAN_PRICING_URL;
window.open(
attributedAmrUrl(destination, attribution, deviceId),
'_blank',
'noopener,noreferrer',
);
}, [
analytics.track,
config.installationId,
config.telemetry?.metrics,
topRightCampaignKind,
workspaceContext?.workspaceId,
]);
// 产品拍板 D5: the campaign modal's paid 立即使用 performs the REAL switch —
// daemon execution mode + Cloud agent (amr) + DeepSeek V4 Flash — through
// the same persistence callbacks the InlineModelSwitcher writes through.
@@ -1681,21 +1622,13 @@ export function EntryShell({
onOpenSearch={() => setProjectSearchOpen(true)}
open={railOpen}
topRightSlot={
view === 'home' && topRightCampaignKind ? (
<button
type="button"
className="entry-deepseek-campaign-badge"
onClick={openCampaignPricing}
aria-label={topRightCampaignKind === 'go'
? goPlanCopy.workbenchBadgeAria
: t('campaign.deepseekV4Flash.workbenchBadgeAria')}
data-testid="deepseek-campaign-pricing-badge"
>
<span>{topRightCampaignKind === 'go'
? goPlanCopy.workbenchBadge
: t('campaign.deepseekV4Flash.workbenchBadge')}</span>
<Icon name="arrow-right" size={13} />
</button>
topRightCampaignKind ? (
<WorkbenchCampaignBadge
kind={topRightCampaignKind}
page="home"
metricsConsent={config.telemetry?.metrics === true}
installationId={config.installationId}
/>
) : null
}
context={railWorkspaceContext}
@@ -1728,9 +1661,8 @@ export function EntryShell({
lives in the rail footer, and everything below is fixed-position
or portalled so it occupies no layout space here. */}
<WhatsNewPopup active={view === 'home'} />
{/* DeepSeek campaign badge moved into EntryNavRail's top-right
cluster (topRightSlot above) so it sits beside the account
module in one flex row. */}
{/* The campaign badge lives in EntryNavRail's top-right cluster so it
stays beside the account module across every entry tab. */}
{amrBalanceGateBlock ? (
<AmrBalanceDialog
reason={amrBalanceGateBlock.reason}
@@ -0,0 +1,101 @@
import { useCallback, useEffect } from 'react';
import {
trackDeepSeekCampaignBadgeClick,
trackDeepSeekCampaignBadgeSurfaceView,
} from '../analytics/events';
import {
amrHandoffDeviceId,
attributedAmrUrl,
recordAmrEntry,
} from '../analytics/amr-attribution';
import { getResolvedDeviceId } from '../analytics/client';
import { useAnalytics } from '../analytics/provider';
import { GO_PLAN_PRICING_URL } from '../campaigns/go-plan';
import { getGoPlanCampaignCopy } from '../campaigns/go-plan-content';
import { useI18n } from '../i18n';
import { Icon } from './Icon';
export type WorkbenchCampaignKind = 'go' | 'deepseek';
export function WorkbenchCampaignBadge({
kind,
page,
metricsConsent,
installationId,
}: {
kind: WorkbenchCampaignKind;
page: 'home' | 'project';
metricsConsent: boolean;
installationId?: string | null;
}) {
const { locale, t } = useI18n();
const analytics = useAnalytics();
const goPlanCopy = getGoPlanCampaignCopy(locale);
useEffect(() => {
// The current campaign analytics contract scopes badge impressions to
// Home. Project-detail visibility is intentionally UI-only until that
// contract gains a project page variant.
if (kind !== 'deepseek' || page !== 'home') return;
trackDeepSeekCampaignBadgeSurfaceView(analytics.track, {
page_name: 'home',
area: 'campaign_badge',
element: 'deepseek_v4_pro',
campaign_id: 'deepseek_v4_pro',
user_state: 'paid',
});
}, [analytics.track, kind, page]);
const openCampaignPricing = useCallback(() => {
if (kind === 'go') {
window.open(GO_PLAN_PRICING_URL, '_blank', 'noopener,noreferrer');
return;
}
if (page === 'home') {
trackDeepSeekCampaignBadgeClick(analytics.track, {
page_name: 'home',
area: 'campaign_badge',
element: 'open_pricing',
campaign_id: 'deepseek_v4_pro',
user_state: 'paid',
});
}
const attribution = recordAmrEntry(
analytics.track,
'deepseek_workbench_badge',
new Date(),
{
metricsConsent,
campaignId: 'deepseek_v4_pro',
conversionSource: 'deepseek_workbench_badge',
},
);
const deviceId = amrHandoffDeviceId({
metricsConsent,
resolvedDeviceId: getResolvedDeviceId(),
installationId,
});
window.open(
attributedAmrUrl(GO_PLAN_PRICING_URL, attribution, deviceId),
'_blank',
'noopener,noreferrer',
);
}, [analytics.track, installationId, kind, metricsConsent, page]);
return (
<button
type="button"
className="entry-deepseek-campaign-badge"
onClick={openCampaignPricing}
aria-label={kind === 'go'
? goPlanCopy.workbenchBadgeAria
: t('campaign.deepseekV4Flash.workbenchBadgeAria')}
data-testid="deepseek-campaign-pricing-badge"
>
<span>{kind === 'go'
? goPlanCopy.workbenchBadge
: t('campaign.deepseekV4Flash.workbenchBadge')}</span>
<Icon name="arrow-right" size={13} />
</button>
);
}
@@ -6,6 +6,18 @@ const entryShellSource = readFileSync(
resolve(process.cwd(), 'src/components/EntryShell.tsx'),
'utf8',
);
const entryNavRailSource = readFileSync(
resolve(process.cwd(), 'src/components/EntryNavRail.tsx'),
'utf8',
);
const appSource = readFileSync(
resolve(process.cwd(), 'src/App.tsx'),
'utf8',
);
const workbenchCampaignBadgeSource = readFileSync(
resolve(process.cwd(), 'src/components/WorkbenchCampaignBadge.tsx'),
'utf8',
);
const entryLayoutStyles = readFileSync(
resolve(process.cwd(), 'src/styles/home/entry-layout.css'),
'utf8',
@@ -59,6 +71,9 @@ describe('DeepSeek V4 Flash workbench campaign entry', () => {
expect(campaignModalStyles).not.toContain(
'.goWelcomeBenefitModel.goWelcomeBenefitZhipu img',
);
expect(campaignModalStyles).toMatch(
/\.goWelcomeBenefitModel img\s*\{[\s\S]*?filter: brightness\(0\) invert\(1\);[\s\S]*?\}/,
);
});
it('collapses the Go modal before its fixed tracks overflow and scrolls short viewports', () => {
@@ -78,22 +93,39 @@ describe('DeepSeek V4 Flash workbench campaign entry', () => {
});
it('reuses the top-right campaign slot for Go and DeepSeek audiences', () => {
expect(entryShellSource).toContain('deepseek-campaign-pricing-badge');
expect(entryShellSource).toContain("topRightCampaignKind === 'go'");
expect(entryShellSource).toContain('goPlanCopy.workbenchBadge');
expect(entryShellSource).toContain("t('campaign.deepseekV4Flash.workbenchBadge')");
expect(entryShellSource).toContain("t('campaign.deepseekV4Flash.workbenchBadgeAria')");
expect(entryShellSource).toContain('<WorkbenchCampaignBadge');
expect(workbenchCampaignBadgeSource).toContain('deepseek-campaign-pricing-badge');
expect(workbenchCampaignBadgeSource).toContain("kind === 'go'");
expect(workbenchCampaignBadgeSource).toContain('goPlanCopy.workbenchBadge');
expect(workbenchCampaignBadgeSource).toContain("t('campaign.deepseekV4Flash.workbenchBadge')");
expect(workbenchCampaignBadgeSource).toContain("t('campaign.deepseekV4Flash.workbenchBadgeAria')");
expect(entryShellSource).toContain("subscriptionAudience === 'unpaid'");
expect(entryShellSource).toContain('goPlanCampaignVisibility.visible');
});
it('keeps the top-right campaign entry visible across entry tabs and project detail', () => {
expect(entryShellSource).toMatch(
/topRightSlot=\{\s*topRightCampaignKind \? \(/,
);
expect(entryShellSource).not.toMatch(
/topRightSlot=\{\s*view === 'home'/,
);
expect(entryNavRailSource).toMatch(
/export function WorkspaceTopRightAccountCluster[\s\S]*?leadingSlot=\{campaignKind \? \([\s\S]*?<WorkbenchCampaignBadge[\s\S]*?page="project"/,
);
expect(appSource).toMatch(
/<WorkspaceTopRightAccountCluster[\s\S]*?amrLoggedIn=\{amrLoginStatus\?\.loggedIn \?\? null\}[\s\S]*?metricsConsent=\{config\.telemetry\?\.metrics === true\}/,
);
});
it('sends both Go and paid DeepSeek badges to public Pricing', () => {
expect(entryShellSource).not.toContain('amrPlansUrlForWorkspace');
expect(entryShellSource).toContain('GO_PLAN_PRICING_URL');
expect(entryShellSource).toContain("'deepseek_workbench_badge'");
expect(entryShellSource).toContain("'noopener,noreferrer'");
expect(workbenchCampaignBadgeSource).toContain('GO_PLAN_PRICING_URL');
expect(workbenchCampaignBadgeSource).toContain("'deepseek_workbench_badge'");
expect(workbenchCampaignBadgeSource).toContain("'noopener,noreferrer'");
// The public URL is locale-neutral; no language is pinned into a link
// shown to every locale.
expect(entryShellSource).not.toContain('open-design.ai/zh/pricing');
expect(workbenchCampaignBadgeSource).not.toContain('open-design.ai/zh/pricing');
});
it('reuses the existing DeepSeek badge treatment without Go-only chrome', () => {
@@ -108,7 +140,7 @@ describe('DeepSeek V4 Flash workbench campaign entry', () => {
expect(entryLayoutStyles).toContain('.entry-deepseek-campaign-badge::before');
expect(entryLayoutStyles).toContain('background: var(--brand-text)');
expect(entryLayoutStyles).toContain('.entry-deepseek-campaign-badge svg');
expect(entryShellSource).toContain('className="entry-deepseek-campaign-badge"');
expect(workbenchCampaignBadgeSource).toContain('className="entry-deepseek-campaign-badge"');
expect(entryLayoutStyles).not.toContain('.entry-go-campaign-new');
expect(entryLayoutStyles).not.toContain('.entry-go-campaign-badge');
expect(badgeRule).not.toContain('color: var(--green)');
@@ -180,9 +212,10 @@ describe('DeepSeek V4 Flash workbench campaign entry', () => {
});
it('keeps existing DeepSeek analytics while the Go pass stays UI-only', () => {
expect(entryShellSource).toContain('trackDeepSeekCampaignBadgeSurfaceView');
expect(entryShellSource).toContain('trackDeepSeekCampaignBadgeClick');
expect(entryShellSource).toContain("window.open(GO_PLAN_PRICING_URL, '_blank', 'noopener,noreferrer')");
expect(workbenchCampaignBadgeSource).toContain('trackDeepSeekCampaignBadgeSurfaceView');
expect(workbenchCampaignBadgeSource).toContain('trackDeepSeekCampaignBadgeClick');
expect(workbenchCampaignBadgeSource).toContain("window.open(GO_PLAN_PRICING_URL, '_blank', 'noopener,noreferrer')");
expect(workbenchCampaignBadgeSource).toContain("page !== 'home'");
expect(modelSwitcherSource).toContain('trackDeepSeekCampaignModelBenefitSurfaceView');
expect(modelSwitcherSource).toContain('trackExecutionSettingsPopoverClick');
});
+18 -2
View File
@@ -14,6 +14,7 @@ describe('Go plan touchpoints', () => {
const start = Date.parse(GO_PLAN_CAMPAIGN.window.startAt);
const end = Date.parse(GO_PLAN_CAMPAIGN.window.endAtExclusive);
expect(GO_PLAN_CAMPAIGN.window.startAt).toBe('2026-08-20T17:00:00+08:00');
expect(isGoPlanCampaignWindowOpen(start - 1)).toBe(false);
expect(isGoPlanCampaignWindowOpen(start)).toBe(true);
expect(isGoPlanCampaignWindowOpen(end - 1)).toBe(true);
@@ -32,8 +33,23 @@ describe('Go plan touchpoints', () => {
});
it('keeps the confirmed Chinese lightweight-entry copy', () => {
expect(getGoPlanCampaignCopy('zh-CN').workbenchBadge).toBe('Go 首月 $5 · 无限用');
expect(getGoPlanCampaignCopy('en').workbenchBadge).toBe('Go first month $5 · unlimited use');
const chinese = getGoPlanCampaignCopy('zh-CN');
const english = getGoPlanCampaignCopy('en');
expect(chinese.workbenchBadge).toBe('全新 Go 套餐 · 首月 ¥5 · 模型无限用');
expect(chinese.headline).toBe('人人可用的低成本设计方案');
expect(chinese.description).toBe(
'以更低成本使用专业设计模型,让每一个想法更快成为作品。',
);
expect(chinese.cta).toBe('查看 Go 套餐 · 限时 5 折');
expect(english.workbenchBadge).toBe(
'The new Go Plan · ¥5 for the first month · Unlimited model usage',
);
expect(english.headline).toBe('Low-cost design plan for everyone');
expect(english.description).toBe(
'Professional design intelligence at a lower cost—so every idea moves faster from prompt to finished work.',
);
expect(english.cta).toBe('View Go plan · Limited-time 50% off');
});
it('ships localized modal and workbench copy for every supported locale', () => {
@@ -49,6 +49,16 @@ function teamContext(): WorkspaceCollabContext {
} as unknown as WorkspaceCollabContext;
}
function freeContext(): WorkspaceCollabContext {
return {
...teamContext(),
workspaceId: 'ws-free',
workspaceType: 'personal',
billingState: 'free',
planId: null,
} as unknown as WorkspaceCollabContext;
}
function idleStatus(): OpenDesignHostUpdaterStatusSnapshot {
return {
arch: 'arm64',
@@ -122,6 +132,7 @@ afterEach(() => {
resetWorkspaceDirectoryCache();
vi.restoreAllMocks();
vi.unstubAllGlobals();
vi.useRealTimers();
});
async function renderWithDownloadedUpdate(context: WorkspaceCollabContext | null = teamContext()) {
@@ -134,6 +145,26 @@ async function renderWithDownloadedUpdate(context: WorkspaceCollabContext | null
}
describe('updater rocket placement after the account avatar', () => {
it('shows the shared Go campaign badge on an unpaid project detail route', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-08-20T10:00:00.000Z'));
render(
<I18nProvider initial="zh-CN">
<WorkspaceTopRightAccountCluster
workspaceContextOverride={freeContext()}
amrLoggedIn
metricsConsent={false}
installationId="test-installation"
/>
</I18nProvider>,
);
expect(screen.getByTestId('deepseek-campaign-pricing-badge').textContent).toContain(
'全新 Go 套餐 · 首月 ¥5 · 模型无限用',
);
});
it('keeps the project-detail updater slot in the shared account row', () => {
render(
<I18nProvider initial="zh-CN">
@@ -25,6 +25,8 @@ export const CAMPAIGN_DISMISSAL_STORAGE: Record<string, string> = {
// id, so a new campaign needs its own entry here — otherwise its modal opens
// over every spec on the first home render, exactly what this file prevents.
'open-design:campaign-seen:deepseek-v4-dual-unlimited-2026': '1',
// Live 8/20-9/3 Go plan launch window.
'open-design:campaign-seen:go-plan-launch-2026': '1',
};
export async function seedCampaignDismissals(context: BrowserContext): Promise<void> {
+14
View File
@@ -66,6 +66,20 @@ test('[P2] captures the Go campaign at narrow and short viewport boundaries', as
await configureVisualPage(page, { projects: [] });
await mockSignedInVelaAccount(page, { plan: 'free' });
await gotoVisualHome(page);
// Functional specs seed campaign dismissals globally so marketing surfaces
// cannot interrupt unrelated flows. This visual contract deliberately opts
// back into the Go modal after establishing the page's same-origin storage.
await page.evaluate(() => {
window.localStorage.removeItem('open-design:campaign-seen:go-plan-launch-2026');
});
await ensureRailOpen(page);
await page.getByTestId('entry-nav-community').evaluate((element: HTMLButtonElement) => {
element.click();
});
await expect(page.getByTestId('entry-view-home')).toHaveAttribute('data-active', 'false');
await page.getByTestId('entry-nav-home').evaluate((element: HTMLButtonElement) => {
element.click();
});
const dialog = page.getByTestId('deepseek-v4-flash-campaign-dialog');
const close = page.getByRole('button', { name: 'Close dialog' });