feat(ui): rewrite marketing/donate pages on clean shadcn, delete their CSS (fast mode) (#823)
Rewrites the static marketing/info surfaces on shadcn primitives (Card / Button / Badge) + Tailwind token utilities, dropping the three legacy page stylesheets. FAST mode: clean shadcn + Tailwind defaults, palette kept (via the existing color-mix + var(--chrome-*) arbitrary utilities), behavior intact, not pixel-perfect. - SupportPage.jsx (SupportView=donate + LicenseView=enterprise): hero, segmented Support/License toggle, Fund-Claude-Max goal Card, amount picker, Ko-fi/PayPal link cards, benefit Cards, and the per-deployment quote panel — all on Card/Button/Badge + Tailwind. All i18n keys, URLs, openExternal, amount state, and view toggling preserved. - ContactPage.jsx: hero + Discord/Email/Issues/Website channel cards rebuilt as hue-tinted Tailwind link rows. - Deleted DonatePage.css (282), EnterprisePage.css (233), SupportPage.css (140) = 655 lines removed; no JS imports them anymore. Kept (shared, untouched): index.css `.lp-aurora*` + `.lp-hero__sweep` (also used by Launchpad). Left the donate widgets (GoalBar/Pip/Postcard) and their DonateGoal.css/Postcard.css in place — already Tailwind-based with genuinely irreducible keyframes (goal-fill grow, Pip bob/wave, postcard stamp/perforation), the sanctioned "small co-located keyframe CSS" exception. The dead no-op `lp-glow-card` class (never defined in CSS) was dropped. Verified: vite build ✓, oxlint 0, oxfmt clean, vitest 641 pass, visual 48 pass, bun install --frozen-lockfile ✓. Eyeballed Donate/Enterprise/Support + Contact in chromium against a stubbed backend — all coherent and on-palette. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
mergetest
Claude Opus 4.8
parent
c931d8f4f8
commit
60ac321a1b
@@ -3,8 +3,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { ArrowLeft, ExternalLink, MessageCircle, Mail, Bug, Globe } from 'lucide-react';
|
||||
import { Button } from '../ui';
|
||||
import { openExternal } from '../api/external';
|
||||
import './DonatePage.css';
|
||||
import './SupportPage.css';
|
||||
|
||||
// All outward contact channels in one place (#contact). Values are the same
|
||||
// ones used across the app (donate/license footers) so there's a single source.
|
||||
@@ -68,31 +66,34 @@ export default function ContactPage({ onBack }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="support-page donate-page flex flex-1 flex-col overflow-y-auto relative isolate bg-[var(--chrome-bg)]">
|
||||
<div className="relative isolate flex flex-1 flex-col overflow-y-auto bg-[var(--chrome-bg)]">
|
||||
<div className="lp-aurora" aria-hidden="true">
|
||||
<span className="lp-aurora__blob lp-aurora__blob--pink" />
|
||||
<span className="lp-aurora__blob lp-aurora__blob--green" />
|
||||
<span className="lp-aurora__blob lp-aurora__blob--amber" />
|
||||
</div>
|
||||
|
||||
<div className="support-page__topbar relative z-[2] flex items-center justify-between gap-[12px] pt-[16px] px-[44px]">
|
||||
<div className="relative z-[2] flex items-center justify-between gap-3 px-11 pt-4">
|
||||
<Button variant="subtle" size="sm" onClick={onBack} leading={<ArrowLeft size={14} />}>
|
||||
{t('donate.back')}
|
||||
</Button>
|
||||
<span className="support-page__spacer w-[96px] shrink-0" aria-hidden="true" />
|
||||
<span className="w-24 shrink-0" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className="support-page__content donate-page__content support-page__content--support relative z-[1] mx-auto flex w-full max-w-[640px] flex-col gap-[24px] px-[32px] pb-[40px]">
|
||||
<div className="support-view">
|
||||
<div className="donate-hero">
|
||||
<div className="donate-hero__icon-wrap">
|
||||
<MessageCircle size={24} className="donate-hero__heart" />
|
||||
</div>
|
||||
<h2 className="donate-hero__title">
|
||||
<div className="relative z-[1] mx-auto flex w-full max-w-[640px] flex-1 flex-col justify-center gap-6 px-8 pb-10">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="text-center">
|
||||
<span className="mx-auto mb-4 flex size-12 items-center justify-center rounded-md border border-[color-mix(in_srgb,#d3869b_30%,transparent)] bg-[color-mix(in_srgb,#d3869b_12%,transparent)]">
|
||||
<MessageCircle
|
||||
size={24}
|
||||
className="text-[#f3a5b6] drop-shadow-[0_0_12px_rgba(243,165,182,0.5)]"
|
||||
/>
|
||||
</span>
|
||||
<h2 className="relative inline-block font-serif text-[2rem] font-normal leading-tight tracking-[-0.02em] text-[var(--chrome-fg)]">
|
||||
{t('contact.hero_title', { defaultValue: 'Get in touch' })}
|
||||
<span className="lp-hero__sweep" aria-hidden="true" />
|
||||
</h2>
|
||||
<p className="donate-hero__subtitle mx-auto mt-[10px] max-w-[480px] font-sans text-[0.8rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
|
||||
<p className="mx-auto mt-2.5 max-w-[480px] font-sans text-[0.8rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
|
||||
{t('contact.hero_desc', {
|
||||
defaultValue:
|
||||
'Questions, bugs, licensing, or just to say hi — here’s how to reach me.',
|
||||
@@ -100,40 +101,37 @@ export default function ContactPage({ onBack }) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="donate-section">
|
||||
<div className="donate-grid support-methods grid grid-cols-[1fr] gap-[10px]">
|
||||
{CHANNELS.map((c, i) => {
|
||||
const Icon = c.icon;
|
||||
return (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
className="donate-card donate-card--link lp-glow-card"
|
||||
style={{ '--anim-i': i, '--card-hue': c.hue }}
|
||||
onClick={() => openExternal(c.url)}
|
||||
>
|
||||
<span className="donate-card__glow" aria-hidden="true" />
|
||||
<div className="donate-card__icon">
|
||||
<Icon size={20} />
|
||||
</div>
|
||||
<div className="donate-card__body">
|
||||
<div className="donate-card__label">
|
||||
{t(c.labelKey, { defaultValue: c.labelDefault })}
|
||||
</div>
|
||||
<div className="donate-card__desc">
|
||||
{t(c.descKey, { defaultValue: c.descDefault })}
|
||||
</div>
|
||||
<div className="contact-card__value mt-[4px] font-mono text-[11px] text-[var(--text-muted,#928374)] opacity-[0.85] [word-break:break-all]">
|
||||
{c.value}
|
||||
</div>
|
||||
</div>
|
||||
<div className="donate-card__arrow">
|
||||
<ExternalLink size={14} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<section className="grid grid-cols-1 gap-2.5">
|
||||
{CHANNELS.map((c) => {
|
||||
const Icon = c.icon;
|
||||
return (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
onClick={() => openExternal(c.url)}
|
||||
style={{ '--card-hue': c.hue }}
|
||||
className="flex w-full items-center gap-3 overflow-hidden rounded-md border border-border bg-transparent px-3.5 py-2.5 text-left transition-colors hover:border-[color-mix(in_srgb,var(--card-hue)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--card-hue)_6%,transparent)]"
|
||||
>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-[color-mix(in_srgb,var(--card-hue)_22%,transparent)] bg-[color-mix(in_srgb,var(--card-hue)_10%,transparent)]">
|
||||
<Icon size={20} />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block font-mono text-xs font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg)]">
|
||||
{t(c.labelKey, { defaultValue: c.labelDefault })}
|
||||
</span>
|
||||
<span className="block font-sans text-[0.68rem] leading-snug text-[var(--chrome-fg-muted)]">
|
||||
{t(c.descKey, { defaultValue: c.descDefault })}
|
||||
</span>
|
||||
<span className="mt-1 block break-all font-mono text-[11px] text-[var(--chrome-fg-muted)] opacity-85">
|
||||
{c.value}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-md border border-border text-[var(--chrome-fg-muted)]">
|
||||
<ExternalLink size={14} />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
/* DonatePage — same chrome frame + aurora system as the Launchpad.
|
||||
Uses design tokens from index.css (:root) and reuses .lp-aurora,
|
||||
.lp-aurora__blob, .lp-hero__sweep, .lp-glow-card from the Launchpad
|
||||
styles so there's zero visual divergence. */
|
||||
|
||||
/* .donate-page + .donate-page__content + .donate-hero__subtitle + .donate-footer
|
||||
+ .donate-social-proof (base) + .donate-amounts are now Tailwind utilities in
|
||||
SupportPage.jsx / ContactPage.jsx. Everything below stays as CSS — it carries
|
||||
keyframes, ::before/::after dividers, :hover combinators, color-mix accents,
|
||||
the --card-hue/--anim-i custom props, and the reduced-motion overrides. */
|
||||
|
||||
/* ── Top bar (Back + Commercial License) ──────────────────── */
|
||||
.donate-page__topbar {
|
||||
padding: 16px 44px 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ── Hero ──────────────────────────────────────────────────── */
|
||||
.donate-hero {
|
||||
text-align: center;
|
||||
padding-top: 24px;
|
||||
animation: donateIn 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.donate-hero__icon-wrap {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
background: color-mix(in srgb, #d3869b 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, #d3869b 30%, transparent);
|
||||
box-shadow: 0 0 32px -4px rgba(211, 134, 155, 0.2);
|
||||
}
|
||||
.donate-hero__heart {
|
||||
color: #f3a5b6;
|
||||
fill: rgba(243, 165, 182, 0.35);
|
||||
filter: drop-shadow(0 0 12px rgba(243, 165, 182, 0.5));
|
||||
animation: donateHeartPulse 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes donateHeartPulse {
|
||||
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 12px rgba(243, 165, 182, 0.4)); }
|
||||
50% { transform: scale(1.08); filter: drop-shadow(0 0 20px rgba(243, 165, 182, 0.7)); }
|
||||
}
|
||||
.donate-hero__title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 2.0rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--chrome-fg);
|
||||
margin: 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
line-height: 1.1;
|
||||
}
|
||||
/* ── Section dividers ─────────────────────────────────────── */
|
||||
.donate-section__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.donate-section__title::before,
|
||||
.donate-section__title::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.12) 1px, transparent 1px);
|
||||
background-size: 6px 1px;
|
||||
}
|
||||
.donate-section__title span {
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: var(--chrome-label-size);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--chrome-label-track);
|
||||
color: var(--chrome-fg-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Card grid ─────────────────────────────────────────────── */
|
||||
.donate-grid--links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
/* ── Shared card chrome ───────────────────────────────────── */
|
||||
.donate-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--chrome-border);
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
transition: background var(--dur-fast), border-color var(--dur-fast), transform var(--dur-base);
|
||||
animation: donateCardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
animation-delay: calc(var(--anim-i, 0) * 80ms);
|
||||
}
|
||||
.donate-card:hover {
|
||||
transform: translateY(-1px);
|
||||
background: color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 6%, transparent);
|
||||
border-color: color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 40%, transparent);
|
||||
box-shadow: 0 10px 24px -12px color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 35%, transparent);
|
||||
}
|
||||
@keyframes donateCardIn {
|
||||
0% { opacity: 0; transform: translateY(10px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Glow layer — reuses the Launchpad's breath/spotlight pattern */
|
||||
.donate-card__glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
border: 1px solid color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 25%, transparent);
|
||||
opacity: 0.15;
|
||||
animation: lpBreath 5.2s ease-in-out infinite;
|
||||
animation-delay: calc(var(--anim-i, 0) * -1.2s);
|
||||
}
|
||||
|
||||
/* Icon bubble */
|
||||
.donate-card__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
background: color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 22%, transparent);
|
||||
font-size: 1.1rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: transform var(--dur-base);
|
||||
}
|
||||
.donate-card:hover .donate-card__icon {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
/* Body text */
|
||||
.donate-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.donate-card__label {
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--chrome-fg);
|
||||
letter-spacing: var(--chrome-label-track);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.donate-card__desc {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.68rem;
|
||||
color: var(--chrome-fg-muted);
|
||||
margin-top: 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* Arrow for link cards */
|
||||
.donate-card__arrow {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
border: 1px solid var(--chrome-border);
|
||||
color: var(--chrome-fg-muted);
|
||||
transition: border-color var(--dur-fast), color var(--dur-fast), background var(--dur-fast);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.donate-card:hover .donate-card__arrow {
|
||||
border-color: color-mix(in srgb, var(--card-hue, #d3869b) 45%, transparent);
|
||||
color: var(--card-hue, #d3869b);
|
||||
background: color-mix(in srgb, var(--card-hue, #d3869b) 8%, transparent);
|
||||
}
|
||||
|
||||
/* ── "Fund Claude Max" goal section + social proof ─────────── */
|
||||
.donate-goal-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid var(--chrome-border);
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--chrome-accent) 4%, transparent);
|
||||
animation: donateCardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.donate-social-proof svg { color: var(--chrome-accent); }
|
||||
|
||||
/* ── Suggested amounts ─────────────────────────────────────── */
|
||||
.donate-amount {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
min-height: 52px;
|
||||
padding: 8px 6px;
|
||||
border: 1px solid var(--chrome-border);
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
background: transparent;
|
||||
color: var(--chrome-fg);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition: background var(--dur-fast), border-color var(--dur-fast), transform var(--dur-base);
|
||||
}
|
||||
.donate-amount:hover {
|
||||
transform: translateY(-1px);
|
||||
background: color-mix(in srgb, var(--chrome-accent) 7%, transparent);
|
||||
border-color: color-mix(in srgb, var(--chrome-accent) 40%, transparent);
|
||||
}
|
||||
.donate-amount.is-selected {
|
||||
border-color: var(--chrome-accent);
|
||||
background: var(--chrome-accent-bg);
|
||||
}
|
||||
.donate-amount__value {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 500;
|
||||
color: var(--chrome-fg);
|
||||
}
|
||||
.donate-amount--common { border-color: color-mix(in srgb, var(--chrome-accent) 35%, transparent); }
|
||||
.donate-amount__badge {
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: 0.54rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--chrome-accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.donate-amount--custom .donate-amount__value {
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--chrome-label-track, 0.08em);
|
||||
color: var(--chrome-fg-muted);
|
||||
}
|
||||
|
||||
/* ── Entry animation ──────────────────────────────────────── */
|
||||
@keyframes donateIn {
|
||||
0% { opacity: 0; transform: translateY(16px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Respect reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.donate-hero,
|
||||
.donate-card,
|
||||
.donate-hero__heart,
|
||||
.donate-card__glow,
|
||||
.donate-goal-section,
|
||||
.donate-amount {
|
||||
animation: none !important;
|
||||
}
|
||||
.donate-amount:hover { transform: none; }
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
/* EnterprisePage — chrome design system, same visual language as
|
||||
Launchpad and DonatePage. Reuses .lp-aurora, .lp-hero__sweep. */
|
||||
|
||||
.enterprise-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
background: var(--chrome-bg);
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.enterprise-page__back {
|
||||
padding: 16px 44px 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.enterprise-page__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 32px 56px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
/* ── Hero ──────────────────────────────────────────────────── */
|
||||
.ent-hero {
|
||||
text-align: center;
|
||||
padding-top: 32px;
|
||||
animation: entIn 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
/* .ent-hero__kicker / .ent-hero__subtitle (base) / .ent-why__grid /
|
||||
.ent-why__label / .ent-why__desc are now Tailwind utilities in
|
||||
SupportPage.jsx (LicenseView). The rest stays — keyframes, ::before/::after
|
||||
dividers, :hover combinators, color-mix accents, !important, details markers. */
|
||||
.ent-hero__title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 2.4rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--chrome-fg);
|
||||
margin: 10px 0 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.ent-hero__subtitle strong {
|
||||
color: var(--chrome-fg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Section title (dotted line + label) ──────────────────── */
|
||||
.ent-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ent-section-title::before,
|
||||
.ent-section-title::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.12) 1px, transparent 1px);
|
||||
background-size: 6px 1px;
|
||||
}
|
||||
.ent-section-title span {
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: var(--chrome-label-size);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--chrome-label-track);
|
||||
color: var(--chrome-fg-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Why grid ─────────────────────────────────────────────── */
|
||||
.ent-why__card {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--chrome-border);
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
transition: background var(--dur-fast), border-color var(--dur-fast);
|
||||
animation: entCardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.ent-why__card:hover {
|
||||
background: var(--chrome-hover-bg);
|
||||
border-color: var(--chrome-border-strong);
|
||||
}
|
||||
.ent-why__icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
background: color-mix(in srgb, #d3869b 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, #d3869b 22%, transparent);
|
||||
color: #d3869b;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
/* ── Pricing — coming-soon panel ──────────────────────────── */
|
||||
.ent-coming-soon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 24px 22px;
|
||||
border: 1px solid var(--chrome-border);
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
background: color-mix(in srgb, #fe8019 5%, transparent);
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
animation: entCardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.ent-coming-soon p {
|
||||
margin: 0;
|
||||
max-width: 540px;
|
||||
color: var(--chrome-fg-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.ent-coming-soon__cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
background: color-mix(in srgb, #fe8019 18%, transparent);
|
||||
border: 1px solid color-mix(in srgb, #fe8019 50%, transparent);
|
||||
color: var(--chrome-fg);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background var(--dur-fast), border-color var(--dur-fast), transform var(--dur-fast);
|
||||
}
|
||||
.ent-coming-soon__cta:hover {
|
||||
background: color-mix(in srgb, #fe8019 28%, transparent);
|
||||
border-color: color-mix(in srgb, #fe8019 70%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ── FAQ ──────────────────────────────────────────────────── */
|
||||
.ent-faq__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.ent-faq__item {
|
||||
border: 1px solid var(--chrome-border);
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ent-faq__item summary {
|
||||
padding: 10px 16px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--chrome-fg);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
transition: background var(--dur-fast);
|
||||
}
|
||||
.ent-faq__item summary::-webkit-details-marker { display: none; }
|
||||
.ent-faq__item summary::before {
|
||||
content: "▸";
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
font-size: 0.65rem;
|
||||
color: var(--chrome-fg-dim);
|
||||
transition: transform var(--dur-fast);
|
||||
}
|
||||
.ent-faq__item[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.ent-faq__item summary:hover {
|
||||
background: var(--chrome-hover-bg);
|
||||
}
|
||||
.ent-faq__item p {
|
||||
margin: 0;
|
||||
padding: 0 16px 12px 32px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.72rem;
|
||||
color: var(--chrome-fg-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── CTA Footer ───────────────────────────────────────────── */
|
||||
.ent-cta-footer {
|
||||
text-align: center;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.ent-cta-footer p {
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: 0.72rem;
|
||||
color: var(--chrome-fg-muted);
|
||||
margin: 0;
|
||||
}
|
||||
.ent-cta-footer__link {
|
||||
background: none;
|
||||
border: none;
|
||||
font: inherit;
|
||||
color: var(--chrome-accent);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
.ent-cta-footer__link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ent-cta-footer__sub {
|
||||
margin-top: 6px !important;
|
||||
font-size: 0.65rem !important;
|
||||
color: var(--chrome-fg-dim) !important;
|
||||
}
|
||||
|
||||
/* ── Animations ───────────────────────────────────────────── */
|
||||
@keyframes entIn {
|
||||
0% { opacity: 0; transform: translateY(16px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes entCardIn {
|
||||
0% { opacity: 0; transform: translateY(10px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ent-hero,
|
||||
.ent-why__card,
|
||||
.ent-tier {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
/* SupportPage — unifies the donate + commercial-license panels behind one
|
||||
segmented toggle. Reuses the donate/enterprise chrome (DonatePage.css,
|
||||
EnterprisePage.css); this file only owns the toggle + panel transitions. */
|
||||
|
||||
/* `.support-page__topbar` base + `.support-page__spacer` base are now Tailwind
|
||||
utilities in SupportPage.jsx / ContactPage.jsx; their @media (max-width:540px)
|
||||
overrides stay below. */
|
||||
|
||||
/* ── Segmented toggle ─────────────────────────────────────────────────── */
|
||||
.support-toggle {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 3px;
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
background: color-mix(in srgb, var(--chrome-fg) 5%, transparent);
|
||||
border: 1px solid var(--chrome-border);
|
||||
isolation: isolate;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* The sliding active background. Carries the panel's accent hue so the toggle
|
||||
itself signals which world you're in — pink for Support, teal for License. */
|
||||
.support-toggle__pill {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
width: calc(50% - 3px);
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
background: color-mix(in srgb, #d3869b 18%, transparent);
|
||||
border: 1px solid color-mix(in srgb, #d3869b 38%, transparent);
|
||||
box-shadow: 0 2px 14px -5px rgba(211, 134, 155, 0.55);
|
||||
z-index: 0;
|
||||
transition:
|
||||
transform 0.36s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
background 0.36s ease,
|
||||
border-color 0.36s ease,
|
||||
box-shadow 0.36s ease;
|
||||
}
|
||||
.support-toggle__pill[data-view="license"] {
|
||||
transform: translateX(100%);
|
||||
background: color-mix(in srgb, #83a598 18%, transparent);
|
||||
border-color: color-mix(in srgb, #83a598 40%, transparent);
|
||||
box-shadow: 0 2px 14px -5px rgba(131, 165, 152, 0.55);
|
||||
}
|
||||
|
||||
.support-toggle__opt {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 18px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: var(--chrome-label-track);
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
color: var(--chrome-fg-muted);
|
||||
cursor: pointer;
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
transition: color 0.22s ease;
|
||||
}
|
||||
.support-toggle__opt svg { transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||
.support-toggle__opt.is-active { color: var(--chrome-fg); }
|
||||
.support-toggle__opt.is-active svg { transform: scale(1.12); }
|
||||
.support-toggle__opt:hover:not(.is-active) { color: var(--chrome-fg); }
|
||||
.support-toggle__opt:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--chrome-accent) 60%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ── Panel cross-fade — the content remounts on toggle (key={view}), which
|
||||
replays the inner hero/card entry animations; this adds a soft lift over
|
||||
the whole panel so the swap feels intentional, not jumpy. ─────────────── */
|
||||
.support-view {
|
||||
animation: supportViewIn 0.42s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
@keyframes supportViewIn {
|
||||
0% { opacity: 0; transform: translateY(10px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Support panel layout ─────────────────────────────────────────────────
|
||||
The Support panel is short, so center it vertically in the available space
|
||||
instead of letting it cling to the top of an empty page. License is tall
|
||||
enough to fill on its own, so it stays top-aligned and scrolls normally. */
|
||||
.support-page__content--support {
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* `.support-methods` (single-column grid) + `.support-chips` (centered flex
|
||||
wrap) are now Tailwind utilities in the JSX. `.support-chip` stays — it has
|
||||
:hover state + transitions + a descendant svg color. */
|
||||
.support-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--chrome-border);
|
||||
border-radius: var(--chrome-radius-pill);
|
||||
background: color-mix(in srgb, var(--chrome-fg) 4%, transparent);
|
||||
color: var(--chrome-fg-muted);
|
||||
font-family: var(--chrome-font-mono);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: var(--chrome-label-track);
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: color var(--dur-fast), border-color var(--dur-fast),
|
||||
background var(--dur-fast), transform var(--dur-base);
|
||||
}
|
||||
.support-chip:hover {
|
||||
color: var(--chrome-fg);
|
||||
border-color: color-mix(in srgb, #d3869b 42%, transparent);
|
||||
background: color-mix(in srgb, #d3869b 8%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.support-chip svg { color: #d3869b; }
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.support-page__topbar { padding: 14px 18px 0; }
|
||||
.support-page__spacer { display: none; }
|
||||
.support-toggle__opt { padding: 6px 12px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.support-toggle__pill,
|
||||
.support-toggle__opt svg { transition: none; }
|
||||
.support-view { animation: none; }
|
||||
}
|
||||
|
||||
/* `.contact-card__value` (the small monospace channel value under each Contact
|
||||
card) is now a Tailwind utility set in ContactPage.jsx. */
|
||||
+183
-131
@@ -13,13 +13,11 @@ import {
|
||||
Star,
|
||||
MessageCircle,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '../ui';
|
||||
import { Button, Badge } from '../ui';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { openExternal } from '../api/external';
|
||||
import GoalBar from '../components/donate/GoalBar';
|
||||
import { loadDonationProgress, BUNDLED_PROGRESS } from '../api/donation';
|
||||
import './DonatePage.css';
|
||||
import './EnterprisePage.css';
|
||||
import './SupportPage.css';
|
||||
|
||||
// GitHub Sponsors isn't available, so donations go through Ko-fi or PayPal and
|
||||
// the supporter picks which — no default-charge nudge, none pre-selected.
|
||||
@@ -43,6 +41,8 @@ const METHODS = [
|
||||
},
|
||||
];
|
||||
|
||||
const DONATE_HUE = '#d3869b';
|
||||
|
||||
// PayPal.me carries the chosen amount straight into the checkout; Ko-fi opens
|
||||
// its tip page (no reliable preset-amount URL). A non-numeric/"custom" amount
|
||||
// falls back to the bare link.
|
||||
@@ -51,28 +51,51 @@ function methodUrl(method, amount) {
|
||||
return method.url;
|
||||
}
|
||||
|
||||
function LinkCard({ method, amount, style }) {
|
||||
const { t } = useTranslation();
|
||||
// Shared "rich link" card — a clickable row with an icon bubble, title + body,
|
||||
// and a trailing external-link affordance. `hue` tints the icon bubble + hover.
|
||||
function LinkCard({ icon, label, desc, value, hue, onClick }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="donate-card donate-card--link lp-glow-card"
|
||||
style={style}
|
||||
onClick={() => openExternal(methodUrl(method, amount))}
|
||||
onClick={onClick}
|
||||
style={{ '--card-hue': hue }}
|
||||
className="flex w-full items-center gap-3 overflow-hidden rounded-md border border-border bg-transparent px-3.5 py-2.5 text-left transition-colors hover:border-[color-mix(in_srgb,var(--card-hue)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--card-hue)_6%,transparent)]"
|
||||
>
|
||||
<span className="donate-card__glow" aria-hidden="true" />
|
||||
<div className="donate-card__icon">{method.icon}</div>
|
||||
<div className="donate-card__body">
|
||||
<div className="donate-card__label">{method.label}</div>
|
||||
<div className="donate-card__desc">{t(method.descriptionKey)}</div>
|
||||
</div>
|
||||
<div className="donate-card__arrow">
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-[color-mix(in_srgb,var(--card-hue)_22%,transparent)] bg-[color-mix(in_srgb,var(--card-hue)_10%,transparent)] text-[1.1rem]">
|
||||
{icon}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block font-mono text-xs font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg)]">
|
||||
{label}
|
||||
</span>
|
||||
<span className="block font-sans text-[0.68rem] leading-snug text-[var(--chrome-fg-muted)]">
|
||||
{desc}
|
||||
</span>
|
||||
{value && (
|
||||
<span className="mt-1 block break-all font-mono text-[11px] text-[var(--chrome-fg-muted)] opacity-85">
|
||||
{value}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-md border border-border text-[var(--chrome-fg-muted)]">
|
||||
<ExternalLink size={14} />
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Section label: a mono uppercase caption with a trailing hairline.
|
||||
function SectionTitle({ children }) {
|
||||
return (
|
||||
<div className="mb-2.5 flex items-center gap-3">
|
||||
<span className="whitespace-nowrap font-mono text-[var(--chrome-label-size)] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg-muted)]">
|
||||
{children}
|
||||
</span>
|
||||
<span className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Support (donate) panel ───────────────────────────────────────────── */
|
||||
function SupportView() {
|
||||
const { t } = useTranslation();
|
||||
@@ -90,25 +113,28 @@ function SupportView() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="support-view">
|
||||
<div className="donate-hero">
|
||||
<div className="donate-hero__icon-wrap">
|
||||
<Heart size={24} className="donate-hero__heart" />
|
||||
</div>
|
||||
<h2 className="donate-hero__title">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="text-center">
|
||||
<span className="mx-auto mb-4 flex size-12 items-center justify-center rounded-md border border-[color-mix(in_srgb,#d3869b_30%,transparent)] bg-[color-mix(in_srgb,#d3869b_12%,transparent)]">
|
||||
<Heart
|
||||
size={24}
|
||||
className="text-[#f3a5b6] [fill:rgba(243,165,182,0.35)] drop-shadow-[0_0_12px_rgba(243,165,182,0.5)]"
|
||||
/>
|
||||
</span>
|
||||
<h2 className="relative inline-block font-serif text-[2rem] font-normal leading-tight tracking-[-0.02em] text-[var(--chrome-fg)]">
|
||||
{t('donate.hero_title')}
|
||||
<span className="lp-hero__sweep" aria-hidden="true" />
|
||||
</h2>
|
||||
<p className="donate-hero__subtitle mx-auto mt-[10px] max-w-[480px] font-sans text-[0.8rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
|
||||
<p className="mx-auto mt-2.5 max-w-[480px] font-sans text-[0.8rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
|
||||
{t('donate.hero_desc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── "Fund Claude Max" goal bar + social proof ──────────────────── */}
|
||||
<section className="donate-section donate-goal-section">
|
||||
<Card className="gap-3 rounded-lg border-border bg-[color-mix(in_srgb,var(--chrome-accent)_4%,transparent)] p-[18px] py-4 shadow-none">
|
||||
<GoalBar progress={progress} />
|
||||
<div className="donate-social-proof flex items-center justify-center gap-[6px] font-mono text-[0.7rem] tracking-[0.01em] text-[var(--chrome-fg-muted)]">
|
||||
<Users size={13} />
|
||||
<div className="flex items-center justify-center gap-1.5 font-mono text-[0.7rem] tracking-[0.01em] text-[var(--chrome-fg-muted)]">
|
||||
<Users size={13} className="text-[var(--chrome-accent)]" />
|
||||
<span>
|
||||
{t('donate.goal.social_proof', {
|
||||
defaultValue: 'Join {{count}} supporters funding local AI',
|
||||
@@ -116,43 +142,56 @@ function SupportView() {
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
|
||||
{/* ── Step 1: pick an amount (none pre-selected; middle is "most common"). ──
|
||||
Selecting only *records* the amount — the supporter then chooses
|
||||
Ko-fi or PayPal below, and PayPal carries the amount through. */}
|
||||
<section className="donate-section">
|
||||
<div className="donate-section__title">
|
||||
<span>{t('donate.suggested_title', { defaultValue: 'Pick an amount' })}</span>
|
||||
</div>
|
||||
<section>
|
||||
<SectionTitle>
|
||||
{t('donate.suggested_title', { defaultValue: 'Pick an amount' })}
|
||||
</SectionTitle>
|
||||
<div
|
||||
className="donate-amounts grid grid-cols-[repeat(4,1fr)] gap-[8px]"
|
||||
className="grid grid-cols-4 gap-2"
|
||||
role="group"
|
||||
aria-label={t('donate.suggested_title', { defaultValue: 'Pick an amount' })}
|
||||
>
|
||||
{SUGGESTED_AMOUNTS.map((a) => (
|
||||
<button
|
||||
key={a.value}
|
||||
type="button"
|
||||
className={`donate-amount ${amount === a.value ? 'is-selected' : ''} ${a.common ? 'donate-amount--common' : ''}`}
|
||||
aria-pressed={amount === a.value}
|
||||
onClick={() => setAmount(amount === a.value ? null : a.value)}
|
||||
>
|
||||
<span className="donate-amount__value">{a.label}</span>
|
||||
{a.common && (
|
||||
<span className="donate-amount__badge">
|
||||
{t('donate.most_common', { defaultValue: 'most common' })}
|
||||
{SUGGESTED_AMOUNTS.map((a) => {
|
||||
const selected = amount === a.value;
|
||||
return (
|
||||
<button
|
||||
key={a.value}
|
||||
type="button"
|
||||
aria-pressed={selected}
|
||||
onClick={() => setAmount(selected ? null : a.value)}
|
||||
className={`flex min-h-[52px] flex-col items-center justify-center gap-0.5 rounded-md border px-1.5 py-2 transition-colors ${
|
||||
selected
|
||||
? 'border-[var(--chrome-accent)] bg-[var(--chrome-accent-bg)]'
|
||||
: `${a.common ? 'border-[color-mix(in_srgb,var(--chrome-accent)_35%,transparent)]' : 'border-border'} hover:border-[color-mix(in_srgb,var(--chrome-accent)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--chrome-accent)_7%,transparent)]`
|
||||
}`}
|
||||
>
|
||||
<span className="font-serif text-[1.05rem] font-medium text-[var(--chrome-fg)]">
|
||||
{a.label}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
{a.common && (
|
||||
<Badge tone="brand" size="xs" className="text-[0.54rem] tracking-[0.06em]">
|
||||
{t('donate.most_common', { defaultValue: 'most common' })}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
className={`donate-amount donate-amount--custom ${amount === 'custom' ? 'is-selected' : ''}`}
|
||||
aria-pressed={amount === 'custom'}
|
||||
onClick={() => setAmount(amount === 'custom' ? null : 'custom')}
|
||||
className={`flex min-h-[52px] flex-col items-center justify-center gap-0.5 rounded-md border px-1.5 py-2 transition-colors ${
|
||||
amount === 'custom'
|
||||
? 'border-[var(--chrome-accent)] bg-[var(--chrome-accent-bg)]'
|
||||
: 'border-border hover:border-[color-mix(in_srgb,var(--chrome-accent)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--chrome-accent)_7%,transparent)]'
|
||||
}`}
|
||||
>
|
||||
<span className="donate-amount__value">
|
||||
<span className="font-mono text-[0.78rem] uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg-muted)]">
|
||||
{t('donate.custom', { defaultValue: 'Custom' })}
|
||||
</span>
|
||||
</button>
|
||||
@@ -161,24 +200,24 @@ function SupportView() {
|
||||
|
||||
{/* ── Step 2: pick where (Ko-fi or PayPal). GitHub Sponsors isn't
|
||||
available; the supporter chooses, and PayPal carries the amount. ── */}
|
||||
<section className="donate-section">
|
||||
<div className="donate-section__title">
|
||||
<span>
|
||||
{typeof amount === 'number'
|
||||
? t('donate.choose_method_amount', {
|
||||
defaultValue: 'Continue with ${{amount}}',
|
||||
amount,
|
||||
})
|
||||
: t('donate.choose_method', { defaultValue: 'Choose how to give' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="donate-grid support-methods grid grid-cols-[1fr] gap-[10px]">
|
||||
{METHODS.map((m, i) => (
|
||||
<section>
|
||||
<SectionTitle>
|
||||
{typeof amount === 'number'
|
||||
? t('donate.choose_method_amount', {
|
||||
defaultValue: 'Continue with ${{amount}}',
|
||||
amount,
|
||||
})
|
||||
: t('donate.choose_method', { defaultValue: 'Choose how to give' })}
|
||||
</SectionTitle>
|
||||
<div className="grid grid-cols-1 gap-2.5">
|
||||
{METHODS.map((m) => (
|
||||
<LinkCard
|
||||
key={m.id}
|
||||
method={m}
|
||||
amount={typeof amount === 'number' ? amount : null}
|
||||
style={{ '--anim-i': i, '--card-hue': '#d3869b' }}
|
||||
icon={m.icon}
|
||||
label={m.label}
|
||||
desc={t(m.descriptionKey)}
|
||||
hue={DONATE_HUE}
|
||||
onClick={() => openExternal(methodUrl(m, typeof amount === 'number' ? amount : null))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -186,29 +225,29 @@ function SupportView() {
|
||||
|
||||
{/* Non-monetary ways to help — gives people who can't (or don't want to)
|
||||
donate a real way to support, and balances out the panel. */}
|
||||
<section className="donate-section">
|
||||
<div className="donate-section__title">
|
||||
<span>{t('support.other_ways')}</span>
|
||||
</div>
|
||||
<div className="support-chips flex flex-wrap justify-center gap-[10px]">
|
||||
<button
|
||||
type="button"
|
||||
className="support-chip"
|
||||
<section>
|
||||
<SectionTitle>{t('support.other_ways')}</SectionTitle>
|
||||
<div className="flex flex-wrap justify-center gap-2.5">
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
leading={<Star size={14} />}
|
||||
onClick={() => openExternal('https://github.com/debpalash/OmniVoice-Studio')}
|
||||
>
|
||||
<Star size={14} /> {t('support.star_github')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="support-chip"
|
||||
{t('support.star_github')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
leading={<MessageCircle size={14} />}
|
||||
onClick={() => openExternal('https://discord.gg/bzQavDfVV9')}
|
||||
>
|
||||
<MessageCircle size={14} /> {t('support.join_discord')}
|
||||
</button>
|
||||
{t('support.join_discord')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="donate-footer pb-[20px] text-center font-mono text-[0.72rem] tracking-[0.02em] text-[var(--chrome-fg-dim)]">
|
||||
<div className="pb-5 text-center font-mono text-[0.72rem] tracking-[0.02em] text-[var(--chrome-fg-dim)]">
|
||||
{t('donate.footer')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,16 +275,16 @@ function LicenseView() {
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="support-view">
|
||||
<div className="ent-hero">
|
||||
<span className="ent-hero__kicker font-mono text-[var(--chrome-label-size)] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg-muted)]">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="text-center">
|
||||
<Badge tone="neutral" size="sm">
|
||||
{t('enterprise.badge')}
|
||||
</span>
|
||||
<h2 className="ent-hero__title">
|
||||
</Badge>
|
||||
<h2 className="relative mt-2.5 inline-block font-serif text-[2.4rem] font-normal leading-tight tracking-[-0.02em] text-[var(--chrome-fg)]">
|
||||
{t('enterprise.hero_title')}
|
||||
<span className="lp-hero__sweep" aria-hidden="true" />
|
||||
</h2>
|
||||
<p className="ent-hero__subtitle mx-auto mt-[16px] max-w-[540px] font-sans text-[0.85rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
|
||||
<p className="mx-auto mt-4 max-w-[540px] font-sans text-[0.85rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
|
||||
{t('enterprise.hero_simple', {
|
||||
defaultValue:
|
||||
'OmniVoice Studio is free and open-source under the AGPL-3.0 — including for commercial and internal business use. You only need a commercial license to embed it in a closed-source product without AGPL’s copyleft obligations.',
|
||||
@@ -253,53 +292,52 @@ function LicenseView() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="ent-why">
|
||||
<div className="ent-why__grid grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-[10px]">
|
||||
{WHY_ITEMS.map(({ icon: Icon, label, desc }) => (
|
||||
<div key={label} className="ent-why__card">
|
||||
<div className="ent-why__icon">
|
||||
<Icon size={16} />
|
||||
</div>
|
||||
<div className="ent-why__label mb-[4px] font-mono text-[0.75rem] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg)]">
|
||||
{label}
|
||||
</div>
|
||||
<div className="ent-why__desc font-sans text-[0.72rem] leading-[1.5] text-[var(--chrome-fg-muted)]">
|
||||
{desc}
|
||||
</div>
|
||||
<section className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-2.5">
|
||||
{WHY_ITEMS.map(({ icon: Icon, label, desc }) => (
|
||||
<Card
|
||||
key={label}
|
||||
className="gap-0 rounded-md border-border bg-transparent p-4 shadow-none transition-colors hover:border-border-strong hover:bg-[var(--chrome-hover-bg)]"
|
||||
>
|
||||
<span className="mb-2.5 flex size-[30px] items-center justify-center rounded-md border border-[color-mix(in_srgb,#d3869b_22%,transparent)] bg-[color-mix(in_srgb,#d3869b_10%,transparent)] text-[#d3869b]">
|
||||
<Icon size={16} />
|
||||
</span>
|
||||
<div className="mb-1 font-mono text-[0.75rem] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg)]">
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="font-sans text-[0.72rem] leading-[1.5] text-[var(--chrome-fg-muted)]">
|
||||
{desc}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* One clear next step: pricing is quoted per deployment, so the action
|
||||
is simply "tell me about your use case". */}
|
||||
<section className="ent-tiers-section">
|
||||
<div className="ent-coming-soon">
|
||||
<p>
|
||||
<section>
|
||||
<Card className="items-center gap-3.5 rounded-md border-border bg-[color-mix(in_srgb,#fe8019_5%,transparent)] p-6 text-center shadow-none">
|
||||
<p className="m-0 max-w-[540px] leading-[1.5] text-[var(--chrome-fg-muted)]">
|
||||
{t('enterprise.contact_lead', {
|
||||
defaultValue:
|
||||
'Pricing is quoted per deployment so it fits your team and workload. Tell me your use case and I’ll get you a quote.',
|
||||
})}
|
||||
</p>
|
||||
<Button
|
||||
variant="subtle"
|
||||
leading={<Mail size={13} />}
|
||||
onClick={() => openExternal(LICENSE_MAILTO)}
|
||||
className="border-[color-mix(in_srgb,#fe8019_50%,transparent)] bg-[color-mix(in_srgb,#fe8019_18%,transparent)] font-semibold text-[var(--chrome-fg)] hover:border-[color-mix(in_srgb,#fe8019_70%,transparent)] hover:bg-[color-mix(in_srgb,#fe8019_28%,transparent)]"
|
||||
>
|
||||
{t('enterprise.request_quote')}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="ent-coming-soon__cta"
|
||||
onClick={() => openExternal(LICENSE_MAILTO)}
|
||||
title={LICENSE_EMAIL}
|
||||
className="font-mono text-[0.65rem] text-[var(--chrome-accent)] hover:underline"
|
||||
>
|
||||
<Mail size={13} />
|
||||
{t('enterprise.request_quote')}
|
||||
{LICENSE_EMAIL}
|
||||
</button>
|
||||
<p className="ent-cta-footer__sub">
|
||||
<button
|
||||
type="button"
|
||||
className="ent-cta-footer__link"
|
||||
onClick={() => openExternal(LICENSE_MAILTO)}
|
||||
title={LICENSE_EMAIL}
|
||||
>
|
||||
{LICENSE_EMAIL}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
@@ -316,8 +354,18 @@ export default function SupportPage({ onBack, initialView = 'support' }) {
|
||||
const { t } = useTranslation();
|
||||
const [view, setView] = useState(initialView === 'license' ? 'license' : 'support');
|
||||
|
||||
// Literal class strings (no interpolation) so Tailwind's JIT can see them.
|
||||
const TAB_BASE =
|
||||
'inline-flex items-center justify-center gap-1.5 rounded-md border px-[18px] py-1.5 font-mono text-[0.72rem] font-semibold uppercase tracking-[var(--chrome-label-track)] whitespace-nowrap transition-colors';
|
||||
const TAB_INACTIVE =
|
||||
'border-transparent text-[var(--chrome-fg-muted)] hover:text-[var(--chrome-fg)]';
|
||||
const TAB_SUPPORT_ACTIVE =
|
||||
'border-[color-mix(in_srgb,#d3869b_38%,transparent)] bg-[color-mix(in_srgb,#d3869b_18%,transparent)] text-[var(--chrome-fg)]';
|
||||
const TAB_LICENSE_ACTIVE =
|
||||
'border-[color-mix(in_srgb,#83a598_40%,transparent)] bg-[color-mix(in_srgb,#83a598_18%,transparent)] text-[var(--chrome-fg)]';
|
||||
|
||||
return (
|
||||
<div className="support-page donate-page flex flex-1 flex-col overflow-y-auto relative isolate bg-[var(--chrome-bg)]">
|
||||
<div className="relative isolate flex flex-1 flex-col overflow-y-auto bg-[var(--chrome-bg)]">
|
||||
{/* Aurora backdrop — shared with the Launchpad */}
|
||||
<div className="lp-aurora" aria-hidden="true">
|
||||
<span className="lp-aurora__blob lp-aurora__blob--pink" />
|
||||
@@ -326,18 +374,21 @@ export default function SupportPage({ onBack, initialView = 'support' }) {
|
||||
</div>
|
||||
|
||||
{/* Top bar: Back (left) · toggle (center) · spacer (right, balances Back) */}
|
||||
<div className="support-page__topbar relative z-[2] flex items-center justify-between gap-[12px] pt-[16px] px-[44px]">
|
||||
<div className="relative z-[2] flex items-center justify-between gap-3 px-11 pt-4">
|
||||
<Button variant="subtle" size="sm" onClick={onBack} leading={<ArrowLeft size={14} />}>
|
||||
{t('donate.back')}
|
||||
</Button>
|
||||
|
||||
<div className="support-toggle" role="tablist" aria-label={t('support.toggle_label')}>
|
||||
<span className="support-toggle__pill" data-view={view} aria-hidden="true" />
|
||||
<div
|
||||
className="grid grid-cols-2 gap-1 rounded-md border border-border bg-[color-mix(in_srgb,var(--chrome-fg)_5%,transparent)] p-[3px]"
|
||||
role="tablist"
|
||||
aria-label={t('support.toggle_label')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={view === 'support'}
|
||||
className={`support-toggle__opt ${view === 'support' ? 'is-active' : ''}`}
|
||||
className={`${TAB_BASE} ${view === 'support' ? TAB_SUPPORT_ACTIVE : TAB_INACTIVE}`}
|
||||
onClick={() => setView('support')}
|
||||
>
|
||||
<Heart size={13} /> {t('support.tab_support')}
|
||||
@@ -346,22 +397,23 @@ export default function SupportPage({ onBack, initialView = 'support' }) {
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={view === 'license'}
|
||||
className={`support-toggle__opt ${view === 'license' ? 'is-active' : ''}`}
|
||||
className={`${TAB_BASE} ${view === 'license' ? TAB_LICENSE_ACTIVE : TAB_INACTIVE}`}
|
||||
onClick={() => setView('license')}
|
||||
>
|
||||
<Building2 size={13} /> {t('support.tab_license')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span className="support-page__spacer w-[96px] shrink-0" aria-hidden="true" />
|
||||
<span className="w-24 shrink-0" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
{/* key={view} remounts the panel so its entry animations replay on toggle.
|
||||
The --support modifier vertically centers the (short) Support panel so
|
||||
it doesn't float at the top of an empty page; License stays top-aligned
|
||||
since it's tall enough to fill on its own. */}
|
||||
The Support panel is short, so it's centered vertically; License is
|
||||
tall enough to fill on its own and stays top-aligned. */}
|
||||
<div
|
||||
className={`support-page__content donate-page__content support-page__content--${view} relative z-[1] mx-auto flex w-full max-w-[640px] flex-col gap-[24px] px-[32px] pb-[40px]`}
|
||||
className={`relative z-[1] mx-auto flex w-full max-w-[640px] flex-col gap-6 px-8 pb-10 ${
|
||||
view === 'support' ? 'flex-1 justify-center' : ''
|
||||
}`}
|
||||
key={view}
|
||||
>
|
||||
{view === 'support' ? <SupportView /> : <LicenseView />}
|
||||
|
||||
Reference in New Issue
Block a user