Revamp the website's donation page

This commit is contained in:
Keavon Chambers
2026-08-28 23:16:22 -07:00
parent ff2afbccda
commit c64a9e5cee
6 changed files with 666 additions and 284 deletions

View File

@@ -2,221 +2,209 @@
title = "Donate"
[extra]
css = ["/page/donate.css", "/component/feature-box.css", "/component/feature-icons.css"]
css = ["/page/donate.css", "/component/feature-box.css"]
js = ["/js/page/donate.js"]
+++
<section>
<div class="diptych donate-hero">
<div class="block">
# Funding creativity, not corporations
**Own your tools. Own your art.** Invest in a sustainable, independent future for high-quality creative software that cannot ever be taken away.
**Own your tools. Own your art.** Graphite is 100% built and funded by our community. Invest in a sustainable, independent future for high-quality creative software that cannot ever be taken away.
<div class="call-to-action">
<span>
<a href="https://github.com/sponsors/GraphiteEditor" target="_blank" class="button arrow">Donate: GitHub Sponsors</a>
<em>Avoids processing fees</em>
</span>
<span>
<a href="#supporter-memberships" class="button arrow">Donate: without an account</a>
<em>Start to finish in several seconds</em>
</span>
</div>
<div class="feature-icons three-wide statistics" data-statistics>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 34" src="https://static.graphite.art/icons/icon-atlas-roadmap__3.png" alt="" />
<span data-statistics-dollars></span>
<div class="statistics" data-statistics>
<div class="statistic">
<span class="value" data-statistics-recurring></span>
<span class="label">In memberships</span>
<span class="note">This month</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 3" src="https://static.graphite.art/icons/icon-atlas-features__2.png" alt="" />
<span data-statistics-members></span>
<div class="statistic">
<span class="value" data-statistics-members></span>
<span class="label">Members</span>
<span class="note">This month</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 47" src="https://static.graphite.art/icons/icon-atlas-roadmap__3.png" alt="" />
<span data-statistics-donors></span>
<div class="statistic">
<span class="value" data-statistics-one-time></span>
<span class="label">In one-time gifts</span>
<span class="note">Past year</span>
</div>
<div class="statistic">
<span class="value" data-statistics-donors></span>
<span class="label">One-time donors</span>
<span class="note">Past year</span>
</div>
</div>
<script>
(async () => {
const element = document.querySelector("[data-statistics]");
const dollarsElement = document.querySelector("[data-statistics-dollars]");
const membersElement = document.querySelector("[data-statistics-members]");
const donorsElement = document.querySelector("[data-statistics-donors]");
if (!dollarsElement || !membersElement || !donorsElement) return;
try {
const response = await fetch("https://graphite.art/sponsorship-stats");
const json = await response.json();
if (!json || !json.recurring || !json.one_time_prior_3_month_sum) throw new Error();
const recurringDollars = parseInt(json.recurring.cents) / 100;
const oneTimeAverageDollars = parseInt(json.one_time_prior_3_month_sum.cents) / 100 / 3;
dollarsElement.innerText = "$" + Math.round(recurringDollars + oneTimeAverageDollars).toLocaleString("en-US") + " / month";
membersElement.innerText = json.recurring.count.toLocaleString("en-US") + " members (supporting monthly)";
donorsElement.innerText = Math.round(json.one_time_prior_3_month_sum.count / 3).toLocaleString("en-US") + " one-time donors (past month)";
// Force repaint to work around Safari bug <https://bugs.webkit.org/show_bug.cgi?id=286403> (remove this and its data attribute when the bug is fixed and widely deployed)
element.style.transform = "scale(1)";
} catch {
element.remove();
}
})();
</script>
Graphite is 100% built and funded by the community. Your contributions directly help us level up the scope and speed of the project's development. Resources are put towards infrastructure, operational costs, swag to keep contributors happy and motivated, and outreach like exhibiting at conventions and traveling to conferences to foster industry relationships. Hiring full-time developers is the next crucial milestone.
</div>
</section>
<section id="supporter-memberships" class="feature-box-outer">
<div class="feature-box-inner">
<div class="block">
<h1 class="feature-box-header">Supporter memberships</h1>
---
Click a membership level below to pay directly by card, no account needed.
A small fee of 3.6% + 30¢ reduces what we receive each month. If convenient, consider instead using <a href="https://github.com/sponsors/GraphiteEditor" target="_blank">GitHub Sponsors</a> for **no fees**.
</div>
<div class="triptych">
<a href="https://buy.stripe.com/6oE2btfCK9863vybII" target="_blank" class="block feature-box-narrow">
<h1 class="feature-box-header">⭕ &ldquo;Quark&rdquo; &raquo;</h1>
**$5 / month**
- Your GitHub profile unlocks a shiny achievement acknowledging your contribution *(through GitHub Sponsors only)*
</a>
<a href="https://buy.stripe.com/00gdUb62aesq9TW7st" target="_blank" class="block feature-box-narrow">
<h1 class="feature-box-header"> &ldquo;Proton&rdquo; &raquo;</h1>
**$10 / month**
- Get a **"Member" role** and accompanying **gold-colored nametag on Discord**
- *Plus the lower-tier rewards*
</a>
<a href="https://buy.stripe.com/5kAbM38aiacaeac28a" target="_blank" class="block feature-box-narrow">
<h1 class="feature-box-header">⚛️ &ldquo;Carbon&rdquo; &raquo;</h1>
**$15 / month**
- Your name/handle listed in the end-of-year **retrospective blog post** thank-you section
- *Plus the lower-tier rewards*
</a>
<a href="https://buy.stripe.com/28o4jB62a0BA5DGbIL" target="_blank" class="block feature-box-narrow">
<h1 class="feature-box-header">🧬 &ldquo;DNA&rdquo; &raquo;</h1>
**$25 / month**
- Your **personal name** (or handle) **on the Graphite website and GitHub readme**
- Option to be mailed a personal **thank-you card with Graphite stickers** (in the US only)
- *Plus the lower-tier rewards*
</a>
<a href="https://buy.stripe.com/28o03laiq0BA8PS6os" target="_blank" class="block feature-box-narrow">
<h1 class="feature-box-header">🌱 &ldquo;Organism&rdquo; &raquo;</h1>
**$50 / month**
- Option to be given a public **shout-out of appreciation** from @GraphiteEditor on your choice of social media sites
- *Plus the lower-tier rewards*
</a>
<a href="https://buy.stripe.com/fZedUbduCfwu2ru7sx" target="_blank" class="block feature-box-narrow">
<h1 class="feature-box-header">🌄 &ldquo;Biosphere&rdquo; &raquo;</h1>
**$75 / month**
- Your personal name (or handle) may be a **hyperlink** to your personal site or social media profile
- *Plus the lower-tier rewards*
</a>
</div>
<div class="block action-buttons">
<a href="https://donate.stripe.com/6oU8wP6m0c2kb2AermbQY0a" target="_blank" class="button arrow">Or make a one-time donation</a>
[Manage your ongoing membership](https://billing.stripe.com/p/login/aEU9EzctSfe3cfK5kk)
</div>
</div>
</section>
<section id="corporate-sponsorships" class="feature-box-outer">
<div class="feature-box-inner">
<div class="block">
<h1 class="feature-box-header">Corporate sponsorships</h1>
---
Also available to individuals wanting to make a larger impact. [Reach out](/contact) to pay by invoice or ACH to avoid fees, or for a custom arrangement.
</div>
<div class="triptych">
<a href="https://buy.stripe.com/7sI6rJ1LU5VUaY05kq" target="_blank" class="block feature-box-narrow">
<h1 class="feature-box-header">🪨 &ldquo;Charcoal&rdquo; &raquo;</h1>
**$100 / month**
- Your **company name** may be shown **on the Graphite website and GitHub readme** starting at this tier level
- *Plus the lower-tier rewards for members*
</a>
<a href="https://buy.stripe.com/3cs8zR8ai0BA8PSaEL" target="_blank" class="block feature-box-narrow">
<h1 class="feature-box-header">🛡️ &ldquo;Carbide&rdquo; &raquo;</h1>
**$250 / month**
- Your name on the website and readme may be a **hyperlink** to your company/personal site
- *Plus the lower-tier rewards for members*
</a>
<a href="https://buy.stripe.com/fZeaHZ76e0BAeaccMU" target="_blank" class="block feature-box-narrow">
<h1 class="feature-box-header">💎 &ldquo;Diamond&rdquo; &raquo;</h1>
**$500 / month**
- Your name and link on the website and readme may instead be a **hyperlinked logo**
- *Plus the lower-tier rewards for members*
</a>
</div>
<div class="block action-buttons">
<a href="https://donate.stripe.com/6oU8wP6m0c2kb2AermbQY0a" target="_blank" class="button arrow">Or make a one-time donation</a>
[Manage your ongoing membership](https://billing.stripe.com/p/login/aEU9EzctSfe3cfK5kk)
</div>
</div>
<div class="donation-picker" id="supporter-memberships">
<div class="frequency">
<label><input type="radio" name="donation-frequency" value="monthly" checked /><span>Monthly</span></label>
<label><input type="radio" name="donation-frequency" value="one-time" /><span>One-Time</span></label>
</div>
<div class="tier-ladder">
<label class="tier"><input type="radio" name="donation-tier" value="quark" /><span class="name">Quark</span><span class="price">$5</span></label>
<label class="tier"><input type="radio" name="donation-tier" value="proton" /><span class="name">Proton</span><span class="price">$10</span></label>
<label class="tier"><input type="radio" name="donation-tier" value="nucleus" /><span class="name">Nucleus</span><span class="price">$15</span></label>
<label class="tier"><input type="radio" name="donation-tier" value="carbon" checked /><span class="name">Carbon</span><span class="price">$25</span></label>
<label class="tier"><input type="radio" name="donation-tier" value="charcoal" /><span class="name">Charcoal</span><span class="price">$50</span></label>
<label class="tier"><input type="radio" name="donation-tier" value="graphite" /><span class="name">Graphite</span><span class="price">$75</span></label>
<label class="higher-reveal"><input type="checkbox" name="higher-tiers" /><span>Higher tiers with the greatest impact &raquo;</span></label>
<label class="tier higher"><input type="radio" name="donation-tier" value="graphene" /><span class="name">Graphene</span><span class="price">$100</span></label>
<label class="tier higher"><input type="radio" name="donation-tier" value="carbide" /><span class="name">Carbide</span><span class="price">$250</span></label>
<label class="tier higher"><input type="radio" name="donation-tier" value="diamond" /><span class="name">Diamond</span><span class="price">$500</span></label>
</div>
<noscript><style>.amount-ladder { display: none !important; }</style></noscript>
<div class="amount-ladder">
<button type="button" class="amount" data-amount="25">$25</button>
<button type="button" class="amount" data-amount="30">$30</button>
<button type="button" class="amount" data-amount="60">$60</button>
<button type="button" class="amount" data-amount="100">$100</button>
<button type="button" class="amount" data-amount="250">$250</button>
<button type="button" class="amount" data-amount="500">$500</button>
</div>
<p class="corporate-link">Sponsoring on behalf of a company? <a href="/contact">Ask about corporate sponsorship.</a></p>
<div class="picker-body">
<div class="payment-controls">
<a href="https://billing.stripe.com/p/login/aEU9EzctSfe3cfK5kk" target="_blank" class="manage">Manage your ongoing membership</a>
<div class="payment-method">
<label><input type="radio" name="donation-method" value="direct" checked /><span>Direct Payment</span></label>
<label><input type="radio" name="donation-method" value="github" /><span>GitHub Sponsors</span></label>
</div>
</div>
<div class="tier-panel" data-tier="quark">
<div class="rewards">
<ul>
<li class="unlocked">Support Graphite's development</li>
<li class="unlocked github-only">Shiny GitHub profile achievement<span class="info" title="Awarded for supporting through GitHub Sponsors, unless you opt for private payments."></span></li>
</ul>
</div>
<a href="https://buy.stripe.com/6oE2btfCK9863vybII" class="button arrow action" data-method="direct">Donate $5 monthly</a>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?tier_id=333371" target="_blank" class="button arrow action" data-method="github">Donate $5 monthly</a>
</div>
<div class="tier-panel" data-tier="proton">
<div class="rewards">
<ul>
<li class="unlocked">Members-only Discord channel and gold nametag</li>
<li class="unlocked">Regular dev sneak-peeks and project files from recent YouTube uploads<span class="info" title="Posted as pinned messages in the members-only Discord channel."></span></li>
<li class="github-only">Shiny GitHub profile achievement<span class="info" title="Awarded for supporting through GitHub Sponsors, unless you opt for private payments."></span></li>
</ul>
</div>
<a href="https://buy.stripe.com/00gdUb62aesq9TW7st" class="button arrow action" data-method="direct">Donate $10 monthly</a>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?tier_id=333372" target="_blank" class="button arrow action" data-method="github">Donate $10 monthly</a>
</div>
<div class="tier-panel" data-tier="nucleus">
<div class="rewards">
<ul>
<li class="unlocked">Your name in our update video and site credits<span class="info" title="Shown in the credits of our YouTube update videos and on this website. Your name is subject to reasonable content approval."></span></li>
<li>Members-only Discord channel and gold nametag</li>
<li>Regular dev sneak-peeks and project files from recent YouTube uploads<span class="info" title="Posted as pinned messages in the members-only Discord channel."></span></li>
<li class="github-only">Shiny GitHub profile achievement<span class="info" title="Awarded for supporting through GitHub Sponsors, unless you opt for private payments."></span></li>
</ul>
</div>
<a href="https://buy.stripe.com/5kAbM38aiacaeac28a" class="button arrow action" data-method="direct">Donate $15 monthly</a>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?tier_id=333373" target="_blank" class="button arrow action" data-method="github">Donate $15 monthly</a>
</div>
<div class="tier-panel" data-tier="carbon">
<div class="rewards">
<ul>
<li class="unlocked">Your name higher in our update video and site credits<span class="info" title="Shown in the credits of our YouTube update videos and on this website. Your name is subject to reasonable content approval."></span></li>
<li>Members-only Discord channel and gold nametag</li>
<li>Regular dev sneak-peeks and project files from recent YouTube uploads<span class="info" title="Posted as pinned messages in the members-only Discord channel."></span></li>
<li class="github-only">Shiny GitHub profile achievement<span class="info" title="Awarded for supporting through GitHub Sponsors, unless you opt for private payments."></span></li>
</ul>
</div>
<a href="https://buy.stripe.com/28o4jB62a0BA5DGbIL" class="button arrow action" data-method="direct">Donate $25 monthly</a>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?tier_id=333374" target="_blank" class="button arrow action" data-method="github">Donate $25 monthly</a>
</div>
<div class="tier-panel" data-tier="charcoal">
<div class="rewards">
<ul>
<li class="unlocked">Your name higher in our update video and site credits<span class="info" title="Shown in the credits of our YouTube update videos and on this website. Your name is subject to reasonable content approval."></span></li>
<li>Members-only Discord channel and gold nametag</li>
<li>Regular dev sneak-peeks and project files from recent YouTube uploads<span class="info" title="Posted as pinned messages in the members-only Discord channel."></span></li>
<li class="github-only">Shiny GitHub profile achievement<span class="info" title="Awarded for supporting through GitHub Sponsors, unless you opt for private payments."></span></li>
</ul>
</div>
<a href="https://buy.stripe.com/28o03laiq0BA8PS6os" class="button arrow action" data-method="direct">Donate $50 monthly</a>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?tier_id=333375" target="_blank" class="button arrow action" data-method="github">Donate $50 monthly</a>
</div>
<div class="tier-panel" data-tier="graphite">
<div class="rewards">
<ul>
<li class="unlocked">Your name higher in our update video and site credits<span class="info" title="Shown in the credits of our YouTube update videos and on this website. Your name is subject to reasonable content approval."></span></li>
<li>Members-only Discord channel and gold nametag</li>
<li>Regular dev sneak-peeks and project files from recent YouTube uploads<span class="info" title="Posted as pinned messages in the members-only Discord channel."></span></li>
<li class="github-only">Shiny GitHub profile achievement<span class="info" title="Awarded for supporting through GitHub Sponsors, unless you opt for private payments."></span></li>
</ul>
</div>
<a href="https://buy.stripe.com/fZedUbduCfwu2ru7sx" class="button arrow action" data-method="direct">Donate $75 monthly</a>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?tier_id=333376" target="_blank" class="button arrow action" data-method="github">Donate $75 monthly</a>
</div>
<div class="tier-panel" data-tier="graphene">
<div class="rewards">
<ul>
<li class="unlocked">Your name higher in our update video and site credits<span class="info" title="Shown in the credits of our YouTube update videos and on this website. Your name is subject to reasonable content approval."></span></li>
<li class="unlocked">Custom hyperlink on your site credits name<span class="info" title="Your link is subject to reasonable content approval."></span></li>
<li>Members-only Discord channel and gold nametag</li>
<li>Regular dev sneak-peeks and project files from recent YouTube uploads<span class="info" title="Posted as pinned messages in the members-only Discord channel."></span></li>
<li class="github-only">Shiny GitHub profile achievement<span class="info" title="Awarded for supporting through GitHub Sponsors, unless you opt for private payments."></span></li>
</ul>
</div>
<a href="https://buy.stripe.com/7sI6rJ1LU5VUaY05kq" class="button arrow action" data-method="direct">Donate $100 monthly</a>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?tier_id=333377" target="_blank" class="button arrow action" data-method="github">Donate $100 monthly</a>
</div>
<div class="tier-panel" data-tier="carbide">
<div class="rewards">
<ul>
<li class="unlocked">Your name higher in our update video and site credits<span class="info" title="Shown in the credits of our YouTube update videos and on this website. Your name is subject to reasonable content approval."></span></li>
<li class="unlocked">Custom avatar picture with your site credits name<span class="info" title="Your avatar is subject to reasonable content approval."></span></li>
<li>Custom hyperlink on your site credits name<span class="info" title="Your link is subject to reasonable content approval."></span></li>
<li>Members-only Discord channel and gold nametag</li>
<li>Regular dev sneak-peeks and project files from recent YouTube uploads<span class="info" title="Posted as pinned messages in the members-only Discord channel."></span></li>
<li class="github-only">Shiny GitHub profile achievement<span class="info" title="Awarded for supporting through GitHub Sponsors, unless you opt for private payments."></span></li>
</ul>
</div>
<a href="https://buy.stripe.com/3cs8zR8ai0BA8PSaEL" class="button arrow action" data-method="direct">Donate $250 monthly</a>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?tier_id=441886" target="_blank" class="button arrow action" data-method="github">Donate $250 monthly</a>
</div>
<div class="tier-panel" data-tier="diamond">
<div class="rewards">
<ul>
<li class="unlocked">Your name higher in our update video and site credits<span class="info" title="Shown in the credits of our YouTube update videos and on this website. Your name is subject to reasonable content approval."></span></li>
<li class="unlocked">A spoken personal thank-you during update video credits</li>
<li>Custom avatar picture with your site credits name<span class="info" title="Your avatar is subject to reasonable content approval."></span></li>
<li>Custom hyperlink on your site credits name<span class="info" title="Your link is subject to reasonable content approval."></span></li>
<li>Members-only Discord channel and gold nametag</li>
<li>Regular dev sneak-peeks and project files from recent YouTube uploads<span class="info" title="Posted as pinned messages in the members-only Discord channel."></span></li>
<li class="github-only">Shiny GitHub profile achievement<span class="info" title="Awarded for supporting through GitHub Sponsors, unless you opt for private payments."></span></li>
</ul>
</div>
<a href="https://buy.stripe.com/fZeaHZ76e0BAeaccMU" class="button arrow action" data-method="direct">Donate $500 monthly</a>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?tier_id=441888" target="_blank" class="button arrow action" data-method="github">Donate $500 monthly</a>
</div>
<div class="tier-panel" data-frequency="one-time">
<div class="rewards">
<ul>
<li>Select a common amount from above or customize it below</li>
<li>We're currently not set up to fulfill membership rewards for one-time donors, but your selfless generosity is appreciated</li>
</ul>
</div>
<form class="one-time-entry" method="POST" action="https://graphite.art/donate/create-donation-session">
<div class="amount-entry">
<label class="amount-field"><span class="currency">$</span><input type="number" name="amount" value="100" min="3" max="10000" step="0.01" required aria-label="Donation amount in US dollars" data-donation-amount /></label>
<button type="submit" class="button arrow action" data-method="direct">Donate</button>
<a href="https://github.com/sponsors/GraphiteEditor/sponsorships?frequency=one-time&amp;amount=100" target="_blank" class="button arrow action" data-method="github" data-github-one-time>Donate</a>
</div>
</form>
</div>
</div>
<div class="picker-footer">
<p class="method-note" data-method="direct">Pay by card or other methods in just a few clicks, no account needed.</p>
<p class="method-note" data-method="github">GitHub subsidizes our processing fees so your full amount reaches us.</p>
</div>
</div>
</div>
</section>

View File

@@ -14,6 +14,7 @@ export default defineConfig([
// Ignore generated directories
"node_modules/",
"public/",
"static/wasm/",
// Ignore vendored code
"static/*.js",
// Don't ignore JS and TS dotfiles in this folder

View File

@@ -1,109 +1,441 @@
.call-to-action.call-to-action.call-to-action {
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: calc(var(--font-size-link) * 0.8);
margin: calc(40 * var(--variable-px)) 0;
// ==================================
// HERO SPLIT BETWEEN COPY AND PICKER
// ==================================
span {
.diptych.donate-hero {
align-items: flex-start;
> .block {
flex: 1 1 420px;
h1 {
text-wrap: balance;
}
}
}
.statistics {
display: grid;
grid-template-columns: repeat(2, 1fr);
// Equal rows keep the two halves balanced against the rule between them
grid-auto-rows: 1fr;
// The copy column aligns its children to the start, so this has to opt into the full width
align-self: stretch;
margin-top: calc(40 * var(--variable-px));
// Hides these while the stats API request is still in flight or has failed
&:has(.value:empty) {
visibility: hidden;
}
.statistic {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: calc(20 * var(--variable-px)) 20px;
em {
text-align: center;
margin-top: 0.5em;
// Each row gets its own rule, inset so the two break apart at the row boundary
&:nth-child(odd) {
position: relative;
&::after {
content: "";
position: absolute;
width: var(--border-thickness);
top: calc(20 * var(--variable-px));
bottom: calc(20 * var(--variable-px));
right: 0;
background-color: var(--color-cloud);
}
}
}
.button {
.value {
font-family: "Bona Nova", Palatino, serif;
font-feature-settings: "lnum";
font-weight: 700;
line-height: 1.25;
font-size: calc(1rem * 20 / 9);
}
.label,
.note {
font-size: calc(1rem * 7 / 9);
font-weight: 800;
text-transform: uppercase;
color: var(--color-walnut);
text-wrap: balance;
}
@media screen and (max-width: 400px) {
grid-template-columns: 1fr;
.statistic:nth-child(odd)::after {
display: none;
}
}
}
// ===============
// DONATION PICKER
// ===============
.donation-picker {
// Shrinks the checkout button, which is the only thing in here sized off this variable
--font-size-link: calc(1rem * 10 / 9);
flex: 1 1 420px;
box-sizing: border-box;
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
padding: calc(var(--feature-box-padding) / 2 * var(--variable-px));
background-color: var(--color-cloud);
background-image: url("https://static.graphite.art/textures/noise.png");
background-blend-mode: overlay;
background-position: center;
// Every visibility rule below is driven by these, so they're taken out of the layout without losing keyboard focusability
input:is([type="radio"], [type="checkbox"]) {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
margin: 0;
}
}
.statistics:has(span:empty) {
visibility: hidden;
}
#supporter-memberships,
#corporate-sponsorships {
.triptych {
gap: 10px;
margin-top: 10px;
a {
flex-basis: calc(100% / 3 - 10px - (var(--feature-box-padding) * var(--variable-px)));
}
}
.triptych + .block,
.feature-box-inner > .block + :is(.block, .triptych) {
margin-top: calc(var(--feature-box-padding) / 2 * var(--variable-px));
}
.triptych + .block {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.action-buttons {
gap: calc(var(--feature-box-padding) / 4 * var(--variable-px)) calc(var(--feature-box-padding) * var(--variable-px));
flex-wrap: wrap;
p {
text-align: left;
text-justify: auto;
white-space: nowrap;
}
}
@media screen and (max-width: 1280px) {
.triptych a {
// Half width, minus own padding on both sides, minus the gap
flex-basis: calc(50% - (var(--feature-box-padding) * var(--variable-px)) - 10px);
}
}
@media screen and (max-width: 640px) {
.triptych a {
flex-basis: 100%;
}
}
.feature-box-narrow {
min-width: 0;
label:has(input:is([type="radio"], [type="checkbox"])) {
background-color: var(--color-fog);
padding: calc(var(--feature-box-padding) / 2 * var(--variable-px));
text-decoration: none;
color: var(--color-navy);
cursor: pointer;
&:hover {
background-color: var(--color-lemon);
}
h1 {
// Compensates for emoji starting with some left spacing
text-indent: -0.2em;
&:has(input:checked) {
background-color: var(--color-navy);
color: var(--color-parchment);
}
&:has(input:focus-visible) {
outline: var(--border-thickness) solid var(--color-crimson);
outline-offset: var(--border-thickness);
}
}
.frequency,
.payment-method {
display: flex;
gap: 10px;
label {
flex: 1 1 0;
text-align: center;
padding: 0.4em 0;
font-weight: 800;
}
}
.frequency {
margin-bottom: calc(20 * var(--variable-px));
}
.tier-ladder,
.amount-ladder {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
@media screen and (max-width: 420px) {
grid-template-columns: repeat(2, 1fr);
}
}
.amount-ladder {
display: none;
}
.amount {
display: flex;
align-items: center;
justify-content: center;
padding: 0.5em 0.25em;
font: inherit;
font-weight: 800;
color: inherit;
border: none;
background-color: var(--color-fog);
cursor: pointer;
&:hover {
background-color: var(--color-lemon);
}
&[aria-pressed="true"] {
background-color: var(--color-navy);
color: var(--color-parchment);
}
&:focus-visible {
outline: var(--border-thickness) solid var(--color-crimson);
outline-offset: var(--border-thickness);
}
}
.tier-ladder {
.tier {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0.5em 0.25em;
.name {
font-weight: 800;
text-transform: uppercase;
font-size: calc(1rem * 7 / 9);
}
.price {
font-size: calc(1rem * 8 / 9);
}
}
.higher-reveal {
grid-column: 1 / -1;
padding: 0.4em 0;
text-align: center;
font-weight: 800;
font-size: calc(1rem * 7 / 9);
text-transform: uppercase;
}
.tier.higher {
display: none;
}
}
&:has(.higher-reveal input:checked) {
.higher-reveal {
display: none;
}
.tier.higher {
display: flex;
}
}
.corporate-link {
text-align: left;
text-justify: auto;
font-size: calc(1rem * 7 / 9);
}
// One toggle serves every tier, yet belongs between each tier's rewards and its button, so fixed rows interleave the two
.picker-body {
display: grid;
grid-template-rows: auto auto auto;
row-gap: 10px;
margin-top: calc(20 * var(--variable-px));
> .tier-panel {
display: none;
}
.rewards {
grid-area: 1 / 1;
}
ul {
padding-left: 1em;
font-size: calc(7 / 9 * 1em);
padding-left: 1.25em;
font-size: calc(1rem * 8 / 9);
}
// Marks what's new in this tier compared to its predecessor
li.unlocked {
font-weight: 700;
}
.info {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.4em;
height: 1.4em;
margin-left: 0.5em;
vertical-align: middle;
position: relative;
top: -0.15em;
border-radius: 50%;
background-color: var(--color-navy);
color: var(--color-parchment);
font-size: calc(1rem * 5 / 9);
font-weight: 700;
line-height: 1;
cursor: help;
&::before {
content: "?";
}
}
> .payment-controls {
grid-area: 2 / 1;
display: flex;
flex-direction: column;
gap: 10px;
// Carried by the toggle rather than the wrapper, so the separation above it never changes
.payment-method {
margin-top: calc(20 * var(--variable-px));
}
}
.manage {
align-self: flex-end;
margin-top: calc(20 * var(--variable-px));
font-size: calc(1rem * 7 / 9);
}
.action {
grid-area: 3 / 1;
display: none;
}
// The amount field serves both methods, so only the buttons beside it swap
.one-time-entry {
grid-area: 3 / 1;
}
.button {
background-color: var(--color-fog);
text-align: center;
}
button.button {
font-family: inherit;
cursor: pointer;
}
.amount-entry {
display: flex;
align-items: center;
gap: 10px;
// The currency sign lives inside the box, so the border belongs to this wrapper instead of the input
.amount-field {
flex: 0 0 auto;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 0.5em;
font-weight: 800;
color: var(--color-navy);
font-size: var(--font-size-link);
height: calc(var(--font-size-link) * 2);
border: var(--border-thickness) solid var(--color-navy);
background-color: white;
cursor: text;
&:focus-within {
outline: var(--border-thickness) solid var(--color-crimson);
outline-offset: var(--border-thickness);
}
}
.currency {
margin-right: 0.25em;
}
input {
width: 6ch;
padding: 0;
font: inherit;
color: inherit;
background: none;
border: none;
appearance: textfield;
&:focus {
outline: none;
}
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
}
.button {
flex: 1 1 auto;
}
}
}
.button {
background-color: var(--color-fog);
.picker-footer {
display: flex;
flex-direction: column;
gap: 0.5em;
font-size: calc(1rem * 7 / 9);
p {
text-align: left;
text-justify: auto;
display: none;
}
}
}
#supporter-memberships {
background-color: var(--color-mustard);
.donation-picker:has([value="quark"]:checked) .tier-panel[data-tier="quark"],
.donation-picker:has([value="proton"]:checked) .tier-panel[data-tier="proton"],
.donation-picker:has([value="nucleus"]:checked) .tier-panel[data-tier="nucleus"],
.donation-picker:has([value="carbon"]:checked) .tier-panel[data-tier="carbon"],
.donation-picker:has([value="charcoal"]:checked) .tier-panel[data-tier="charcoal"],
.donation-picker:has([value="graphite"]:checked) .tier-panel[data-tier="graphite"],
.donation-picker:has([value="graphene"]:checked) .tier-panel[data-tier="graphene"],
.donation-picker:has([value="carbide"]:checked) .tier-panel[data-tier="carbide"],
.donation-picker:has([value="diamond"]:checked) .tier-panel[data-tier="diamond"] {
display: contents;
}
#corporate-sponsorships {
background-color: var(--color-ale);
margin-top: 0;
.donation-picker:has([value="one-time"]:checked) {
.tier-ladder {
display: none;
}
.amount-ladder {
display: grid;
}
.picker-body .tier-panel[data-tier] {
display: none;
}
.tier-panel[data-frequency="one-time"] {
display: contents;
}
// There is no ongoing membership to manage from a one-time gift
.manage {
display: none;
}
}
.donation-picker:has([value="direct"]:checked) .github-only {
color: Rgb(from var(--color-navy) r g b / 0.33);
}
.donation-picker:has([value="github"]:checked) .manage {
color: Rgb(from var(--color-crimson) r g b / 0.33);
pointer-events: none;
}
.donation-picker:has([value="direct"]:checked) :is(.action, .method-note)[data-method="direct"],
.donation-picker:has([value="github"]:checked) :is(.action, .method-note)[data-method="github"] {
display: block;
}

View File

@@ -0,0 +1,60 @@
const statisticsRequest = fetch("https://graphite.art/donate/sponsorship-stats").then((response) => response.json());
window.addEventListener("DOMContentLoaded", () => {
setUpAmountPresets();
showStatistics();
});
function setUpAmountPresets() {
const amountField = /** @type {HTMLInputElement | null} */ (document.querySelector("[data-donation-amount]"));
const presetButtons = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll("[data-amount]"));
const githubOneTime = /** @type {HTMLAnchorElement | null} */ (document.querySelector("[data-github-one-time]"));
if (!amountField || presetButtons.length === 0) return;
const syncAmount = () => {
presetButtons.forEach((button) => button.setAttribute("aria-pressed", String(button.dataset.amount === amountField.value)));
if (!githubOneTime) return;
const amount = Number(amountField.value);
const suffix = Number.isFinite(amount) && amount > 0 ? `&amount=${amount}` : "";
githubOneTime.href = `https://github.com/sponsors/GraphiteEditor/sponsorships?frequency=one-time${suffix}`;
};
presetButtons.forEach((button) => {
button.addEventListener("click", () => {
amountField.value = button.dataset.amount || "";
syncAmount();
});
});
amountField.addEventListener("input", syncAmount);
syncAmount();
}
async function showStatistics() {
const element = /** @type {HTMLElement | null} */ (document.querySelector("[data-statistics]"));
const recurringElement = /** @type {HTMLElement | null} */ (document.querySelector("[data-statistics-recurring]"));
const membersElement = /** @type {HTMLElement | null} */ (document.querySelector("[data-statistics-members]"));
const oneTimeElement = /** @type {HTMLElement | null} */ (document.querySelector("[data-statistics-one-time]"));
const donorsElement = /** @type {HTMLElement | null} */ (document.querySelector("[data-statistics-donors]"));
if (!element || !recurringElement || !membersElement || !oneTimeElement || !donorsElement) return;
try {
const json = await statisticsRequest;
if (!json || !json.recurring || !json.one_time_prior_12_month_sum) throw new Error();
const number = (/** @type {number} */ value) => Math.round(value).toLocaleString("en-US", { useGrouping: "min2" });
const dollars = (/** @type {number} */ value) => `$${number(value)}`;
recurringElement.innerText = dollars(parseInt(json.recurring.cents) / 100);
membersElement.innerText = number(json.recurring.count);
oneTimeElement.innerText = dollars(parseInt(json.one_time_prior_12_month_sum.cents) / 100);
donorsElement.innerText = number(json.one_time_prior_12_month_sum.count);
// Force repaint to work around Safari bug <https://bugs.webkit.org/show_bug.cgi?id=286403> (remove this and its data attribute when the bug is fixed and widely deployed)
element.style.transform = "scale(1)";
} catch {
element.remove();
}
}

View File

@@ -152,7 +152,7 @@
<a href="/press" class="link not-uppercase">Press</a>
<a href="/contact" class="link not-uppercase">Contact</a>
</nav>
<span>Copyright &copy; {{ now() | date(format = "%Y") }} Graphite Labs, LLC (an open source community organization)</span>
<span>Copyright &copy; {{ now() | date(format = "%Y") }} Graphite Labs, an Open Source Community Organization</span>
</footer>
</div>
</body>

View File

@@ -24,6 +24,7 @@
// Ignore generated directories
"node_modules",
"public",
"static/wasm",
// Ignore vendored code
"static/*.js"
]