From 078995d179f1e998b6aa2e60379302ad5e75972c Mon Sep 17 00:00:00 2001
From: Naman
Date: Sun, 2 Aug 2026 17:41:58 +0200
Subject: [PATCH 01/12] Blue default ink, always-dark canvas chrome, smooth
theme fade, sponsors, showcase
- Default pen color is now blue; sticky notes still default to yellow paper
when the pen is on the default ink.
- The app's logo, file bar, and files menu now share the dock's dark glass
in both themes, so every bar on the canvas stays dark in light mode.
- Theme switches crossfade: the outgoing canvas is frozen as a bitmap and
faded out (respects prefers-reduced-motion), and the board background
transitions in CSS.
- New sponsors system on the website: config-driven wall (lib/sponsors.js),
homepage section with a designed empty state and per-tier caps that roll
overflow into a dedicated /sponsors page, payments via GitHub Sponsors,
plus .github/FUNDING.yml for the repo Sponsor button.
- README gains Made with Quickdraw + Sponsors sections; CONTRIBUTING.md
documents the add-your-project-via-PR flow.
Co-Authored-By: Claude Fable 5
---
.github/FUNDING.yml | 3 +
CONTRIBUTING.md | 15 ++
README.md | 19 ++
apps/app/index.html | 87 ++++----
apps/app/src/main.js | 7 +-
apps/website/src/components/Sponsors.astro | 177 ++++++++++++++++
apps/website/src/layouts/Base.astro | 1 +
apps/website/src/lib/sponsors.js | 45 ++++
apps/website/src/pages/index.astro | 3 +
apps/website/src/pages/sponsors.astro | 236 +++++++++++++++++++++
packages/core/src/editor.js | 36 +++-
packages/core/src/quickdraw.css | 13 ++
packages/core/src/ui.js | 2 +-
packages/core/test/editor.test.js | 2 +-
14 files changed, 592 insertions(+), 54 deletions(-)
create mode 100644 .github/FUNDING.yml
create mode 100644 apps/website/src/components/Sponsors.astro
create mode 100644 apps/website/src/lib/sponsors.js
create mode 100644 apps/website/src/pages/sponsors.astro
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..ce5a470
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# Shows the "Sponsor" button on the repo page.
+github: nmndwivedi
+custom: ['https://tryquickdraw.com/sponsors/']
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2e0d778..f4e149d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,3 +36,18 @@ fastest way to poke the raw engine.
- Hand-written type declarations live in each package's `types/index.d.ts`;
update them together with API changes.
- Keep comments in the existing voice: explain *why*, not *what*.
+
+## Add your project to the showcase
+
+Built something with Quickdraw? Add it to the
+["Made with Quickdraw" table in the README](README.md#made-with-quickdraw) —
+it's the standard drill:
+
+1. Edit `README.md` and add one row to the table, keeping it alphabetical.
+2. Link the project name to a live URL (or the repo if it isn't public yet),
+ and keep the description to 15 words or fewer, no superlatives.
+3. Open a PR titled `showcase: `. Nothing else in the diff.
+
+Any real project that ships Quickdraw qualifies — side projects included.
+We only skip entries that are dead links, NSFW, or pure landing pages with no
+product behind them.
diff --git a/README.md b/README.md
index 4d98f4d..e4e54ed 100644
--- a/README.md
+++ b/README.md
@@ -220,6 +220,25 @@ layers, frames, or rich text. The full roadmap lives in
[issue #1](https://github.com/nmndwivedi/quickdraw/issues/1) — comment there
if the missing feature that sent you back to a paid SDK is on it (or isn't).
+## Made with Quickdraw
+
+Projects and products built on Quickdraw. Shipped something? **Add it here** —
+edit this table and open a PR (one row, alphabetical, ≤ 15-word description;
+see the [showcase guidelines](CONTRIBUTING.md#add-your-project-to-the-showcase)).
+
+| Project | Description |
+| --- | --- |
+| [tryquickdraw.com/app](https://app.tryquickdraw.com) | The official free whiteboard app — no account, autosaves locally. |
+| _Your project here_ | [Add yours →](CONTRIBUTING.md#add-your-project-to-the-showcase) |
+
+## Sponsors
+
+Quickdraw is MIT-licensed with no paid tier — [sponsors](https://tryquickdraw.com/sponsors/)
+are what keep it that way. Gold and Silver sponsors get their logo here and on
+the website:
+
+No sponsors yet — your logo could be the first.
+
## Star history
Quickdraw is free forever — no license fees, no upsell. If it saves you one, a
diff --git a/apps/app/index.html b/apps/app/index.html
index 2f6c7ee..163fb60 100644
--- a/apps/app/index.html
+++ b/apps/app/index.html
@@ -16,57 +16,65 @@
diff --git a/apps/website/src/layouts/Base.astro b/apps/website/src/layouts/Base.astro
index 09c0561..7634c09 100644
--- a/apps/website/src/layouts/Base.astro
+++ b/apps/website/src/layouts/Base.astro
@@ -66,6 +66,7 @@ const canonical = new URL(Astro.url.pathname, SITE_URL).href
IssuesDiscussionsChangelog
+ Sponsors
Packages
diff --git a/apps/website/src/lib/sponsors.js b/apps/website/src/lib/sponsors.js
new file mode 100644
index 0000000..4a31630
--- /dev/null
+++ b/apps/website/src/lib/sponsors.js
@@ -0,0 +1,45 @@
+// Sponsorship config — the single source of truth for tiers and the wall.
+// Payments run through GitHub Sponsors; if the platform ever changes, this
+// URL is the only thing to touch.
+export const SPONSOR_URL = 'https://github.com/sponsors/nmndwivedi'
+
+// Highest tier first — every list on the site renders in this order.
+export const TIERS = [
+ {
+ id: 'gold',
+ name: 'Gold',
+ amount: '$100/mo',
+ hue: '#d97706',
+ perks: ['Large logo on the homepage and sponsors page', 'Logo in the README', 'Priority on bug reports'],
+ },
+ {
+ id: 'silver',
+ name: 'Silver',
+ amount: '$25/mo',
+ hue: '#64748b',
+ perks: ['Logo on the homepage and sponsors page', 'Name in the README'],
+ },
+ {
+ id: 'backer',
+ name: 'Backer',
+ amount: '$5/mo',
+ hue: '#2f6fed',
+ perks: ['Name on the sponsors page', 'Our sincere gratitude, forever'],
+ },
+]
+
+// The sponsor wall. After sponsoring, add yourself here via PR (or open an
+// issue and we'll do it): { name, url, tier, logo?, since?, past? }
+// name — company or person
+// url — where the logo/name links
+// tier — 'gold' | 'silver' | 'backer'
+// logo — optional path under /public/sponsors/ (SVG with transparent
+// background preferred; falls back to a text chip without one)
+// since — 'YYYY-MM', shown on the sponsors page
+// past — true moves the entry to the "past sponsors" list; we never
+// silently delete anyone who supported the project
+export const SPONSORS = []
+
+export const activeSponsors = () => SPONSORS.filter((s) => !s.past)
+export const pastSponsors = () => SPONSORS.filter((s) => s.past)
+export const byTier = (tierId) => activeSponsors().filter((s) => s.tier === tierId)
diff --git a/apps/website/src/pages/index.astro b/apps/website/src/pages/index.astro
index b10f7ee..6b2ed56 100644
--- a/apps/website/src/pages/index.astro
+++ b/apps/website/src/pages/index.astro
@@ -3,6 +3,7 @@ import Base from '../layouts/Base.astro'
import CompareTable from '../components/CompareTable.astro'
import StarButton from '../components/StarButton.astro'
import StarHistory from '../components/StarHistory.astro'
+import Sponsors from '../components/Sponsors.astro'
import { readStarHistory } from '../lib/starData.js'
import { APP_URL, GITHUB_URL, SITE_URL } from '../consts.js'
@@ -286,6 +287,8 @@ createQuickdraw({
+
+
Questions people ask
diff --git a/apps/website/src/pages/sponsors.astro b/apps/website/src/pages/sponsors.astro
new file mode 100644
index 0000000..963ddf3
--- /dev/null
+++ b/apps/website/src/pages/sponsors.astro
@@ -0,0 +1,236 @@
+---
+// The full sponsor wall — uncapped, unlike the homepage section. This page
+// always exists so "too many sponsors for the homepage" is never a problem:
+// the homepage rolls overflow up into a "+N more" link that lands here.
+import Base from '../layouts/Base.astro'
+import { GITHUB_URL } from '../consts.js'
+import { SPONSOR_URL, TIERS, byTier, pastSponsors, activeSponsors } from '../lib/sponsors.js'
+
+const past = pastSponsors()
+const empty = activeSponsors().length === 0
+
+const steps = [
+ {
+ title: 'Pick a tier',
+ body: 'Sponsorship runs through GitHub Sponsors — a standard GitHub checkout, monthly or one-time, with receipts and invoices handled for you.',
+ },
+ {
+ title: 'Send your logo',
+ body: 'Open a PR adding yourself to sponsors.js (an SVG with a transparent background works best), or just open an issue and we’ll do it for you.',
+ },
+ {
+ title: 'You’re on the wall',
+ body: 'Your logo ships with the next deploy — on this page, the homepage, and (Gold and Silver) the README on GitHub and npm.',
+ },
+]
+---
+
+
+
+
Sponsor Quickdraw
+
+ Quickdraw is MIT-licensed and free for commercial use, forever. There is
+ no paid tier waiting behind a curtain — sponsorship is the entire
+ business model. In return, your logo sits in front of every developer
+ who is actively choosing a whiteboard SDK.
+
+ One-time donations of any size are welcome too. Want something a tier
+ doesn't cover — a support arrangement, a feature bounty?
+ Start a discussion.
+
+
+
+
+
The wall
+ {empty ? (
+
+
+
Nobody's here yet.
+
+ Which means the first sponsors get the one thing money usually
+ can't buy on a sponsor wall: being first. Founding sponsors stay
+ marked as such for the life of the project.
+
+ Sponsorship never buys influence over the roadmap or the license —
+ Quickdraw stays MIT either way. Logos link with
+ rel="sponsored", cancel anytime, and past sponsors are
+ acknowledged here permanently.
+
+
+
+
+
+
diff --git a/packages/core/src/editor.js b/packages/core/src/editor.js
index 28bba64..6a29560 100644
--- a/packages/core/src/editor.js
+++ b/packages/core/src/editor.js
@@ -18,7 +18,7 @@ const RESIZE_CURSORS = {
tl: 'nwse-resize', br: 'nwse-resize', tr: 'nesw-resize', bl: 'nesw-resize',
t: 'ns-resize', b: 'ns-resize', l: 'ew-resize', r: 'ew-resize',
}
-const DEFAULT_STYLES = { color: 'black', size: 'm', dash: 'draw', fill: 'none', font: 'draw' }
+const DEFAULT_STYLES = { color: 'blue', size: 'm', dash: 'draw', fill: 'none', font: 'draw' }
export const TOOLS = ['select', 'hand', 'draw', 'highlight', 'eraser', 'laser', 'arrow', 'line', 'geo', 'text', 'note']
@@ -260,11 +260,41 @@ export class Editor {
setTheme(id) {
const t = themeOf(id)
if (t === this.theme) return
+ this._crossfadeTheme()
this.theme = t
this.container.dataset.qdTheme = t.id
this.requestRender()
this.emit('theme')
}
+ // Freeze the outgoing theme as a bitmap over the board and fade it out, so
+ // a theme switch melts from one paper to the other instead of hard-cutting.
+ _crossfadeTheme() {
+ if (this._destroyed) return
+ if (typeof window !== 'undefined' &&
+ window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return
+ const w = this.canvas.width, h = this.canvas.height
+ if (!w || !h) return
+ try {
+ const snap = document.createElement('canvas')
+ snap.width = w
+ snap.height = h
+ snap.getContext('2d').drawImage(this.canvas, 0, 0)
+ snap.className = 'qd-theme-fade'
+ this._themeFade?.remove()
+ this._themeFade = snap
+ this.overlay.after(snap)
+ // two frames: one to paint at full opacity, one to start the transition
+ requestAnimationFrame(() => requestAnimationFrame(() => { snap.style.opacity = '0' }))
+ const done = () => {
+ snap.remove()
+ if (this._themeFade === snap) this._themeFade = null
+ }
+ snap.addEventListener('transitionend', done, { once: true })
+ setTimeout(done, 600) // safety net if transitionend never fires
+ } catch {
+ // a stubbed 2D context (tests, exotic embeds) just skips the fade
+ }
+ }
// 'none' | 'lines' | 'dots' — the backdrop behind the drawing
setGrid(id) {
if (!GRID_IDS.includes(id) || id === this.grid) return
@@ -871,7 +901,7 @@ export class Editor {
this.store.put({
id, typeName: 'shape', type: 'note', x: p.x - NOTE_W / 2, y: p.y - NOTE_W / 2, rot: 0,
z: this.store.maxZ() + 1,
- props: { text: '', color: this.styles.color === 'black' ? 'yellow' : this.styles.color, size: 'm', font: this.styles.font, scale: 1 },
+ props: { text: '', color: this.styles.color === DEFAULT_STYLES.color ? 'yellow' : this.styles.color, size: 'm', font: this.styles.font, scale: 1 },
})
this.setTool('select')
this.setSelection([id])
@@ -1818,6 +1848,8 @@ export class Editor {
destroy() {
this._destroyed = true
this._commitText()
+ this._themeFade?.remove()
+ this._themeFade = null
cancelAnimationFrame(this._raf)
cancelAnimationFrame(this._camAnim)
cancelAnimationFrame(this._fitEaseRaf || 0)
diff --git a/packages/core/src/quickdraw.css b/packages/core/src/quickdraw.css
index 79f2c01..f7bbf5d 100644
--- a/packages/core/src/quickdraw.css
+++ b/packages/core/src/quickdraw.css
@@ -14,6 +14,19 @@
container-type: inline-size;
}
.qd-root[data-qd-theme='dark'] { background: #191713; }
+.qd-root { transition: background 320ms ease; }
+
+/* snapshot of the outgoing theme, faded out by the editor on setTheme() */
+.qd-theme-fade {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 20; /* over canvas + overlay, under the floating chrome */
+ pointer-events: none;
+ opacity: 1;
+ transition: opacity 320ms ease;
+}
.qd-canvas, .qd-overlay {
position: absolute;
diff --git a/packages/core/src/ui.js b/packages/core/src/ui.js
index 3469615..f07f44f 100644
--- a/packages/core/src/ui.js
+++ b/packages/core/src/ui.js
@@ -481,7 +481,7 @@ export function buildUI(editor, { hidden = false, onSave, themeToggle = true, gr
// the folded button wears the active tool so the state stays visible
toolsBtn.innerHTML = ICONS[editor.tool === 'geo' ? editor.geoKind : editor.tool] || ICONS.select
toolsBtn.classList.toggle('on', popover?.name === 'tools')
- styleDot.style.background = editor.theme.colors[editor.currentStyles().color || 'black'].stroke
+ styleDot.style.background = editor.theme.colors[editor.currentStyles().color || 'blue'].stroke
menuBtn.classList.toggle('on', popover?.name === 'menu')
styleBtn.classList.toggle('on', popover?.name === 'styles')
moreBtn.classList.toggle('on', popover?.name === 'more')
diff --git a/packages/core/test/editor.test.js b/packages/core/test/editor.test.js
index c9c7f5a..a8b3872 100644
--- a/packages/core/test/editor.test.js
+++ b/packages/core/test/editor.test.js
@@ -255,7 +255,7 @@ describe('text & notes', () => {
expect(editor.store.shapes().length).toBe(1)
})
- it('notes get the note default color when pen is black', () => {
+ it('notes get the note default color when the pen is on the default ink', () => {
editor.setTool('note')
drag(editor, [[50, 50]])
const note = editor.store.shapes().find((s) => s.type === 'note')
From 4966498767d536e4bbf26badbbf823a16e57d91d Mon Sep 17 00:00:00 2001
From: Naman
Date: Sun, 2 Aug 2026 17:51:11 +0200
Subject: [PATCH 02/12] Make all canvas chrome follow the theme, with a smooth
switch
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The dock, actions pill, popovers, and the app's logo/file bar/files menu
were dark glass in both themes. They now theme properly — light glass with
dark ink on the light paper, the original dark glass on dark — driven by
--qd-* custom properties in quickdraw.css (and matching vars in the app's
index.html). The surfaces transition background/color/border over 320ms,
the same clock as the board's theme crossfade, so the whole switch reads
as one smooth motion.
Co-Authored-By: Claude Fable 5
---
apps/app/index.html | 74 +++++++++++-----
apps/app/src/main.js | 5 +-
packages/core/src/quickdraw.css | 150 ++++++++++++++++++++++----------
3 files changed, 158 insertions(+), 71 deletions(-)
diff --git a/apps/app/index.html b/apps/app/index.html
index 163fb60..ae4e9c3 100644
--- a/apps/app/index.html
+++ b/apps/app/index.html
@@ -16,25 +16,53 @@
diff --git a/apps/website/src/components/Sponsors.astro b/apps/website/src/components/Sponsors.astro
index 3470d3f..53457f4 100644
--- a/apps/website/src/components/Sponsors.astro
+++ b/apps/website/src/components/Sponsors.astro
@@ -1,19 +1,11 @@
---
-// The homepage sponsor wall — one flat list, no tiers, no prices. Empty, it
-// sells the spot itself with ghost slots; populated, it's a simple row of
-// sponsors plus a dashed "your logo" slot as the standing invite. Overflow
-// past the cap rolls up into "+N more →" pointing at /sponsors.
+// Homepage sponsors section — heading and lede around the shared wall.
+// The empty wall carries its own CTA, so the extra button only appears
+// once there are sponsors and the pitch card is gone.
+import SponsorWall from './SponsorWall.astro'
import { SPONSOR_URL, sponsors } from '../lib/sponsors.js'
-const all = sponsors()
-const CAP = 11 // with the invite slot that's two tidy rows at desktop width
-const shown = all.slice(0, CAP)
-const overflow = all.length - shown.length
-const empty = all.length === 0
-
-// The empty wall: six ghost slots fading out to the right, so the space reads
-// as "waiting for logos", not "broken".
-const ghosts = [1, 0.72, 0.52, 0.36, 0.24, 0.15]
+const hasSponsors = sponsors().length > 0
---
diff --git a/apps/website/src/pages/sponsors.astro b/apps/website/src/pages/sponsors.astro
index 1555c1f..5684a9f 100644
--- a/apps/website/src/pages/sponsors.astro
+++ b/apps/website/src/pages/sponsors.astro
@@ -1,13 +1,10 @@
---
-// The sponsors page — a simple invite that points at GitHub Sponsors. No
-// tiers, no prices: the wall is one flat list that syncs from the GitHub
-// Sponsors API daily (see .github/workflows/sponsors.yml).
+// The sponsors page — a simple invite that points at GitHub Sponsors, and
+// the same wall component the homepage renders (uncapped here).
import Base from '../layouts/Base.astro'
+import SponsorWall from '../components/SponsorWall.astro'
import { GITHUB_URL } from '../consts.js'
-import { SPONSOR_URL, sponsors } from '../lib/sponsors.js'
-
-const all = sponsors()
-const empty = all.length === 0
+import { SPONSOR_URL } from '../lib/sponsors.js'
---
The wall
- {empty ? (
-
-
-
Nobody's here yet.
-
- Which means the first sponsors get the one thing money usually
- can't buy on a sponsor wall: being first. Founding sponsors stay
- marked as such for the life of the project.
-
The wall syncs with GitHub
Sponsors daily — sponsor there and you appear here automatically,
@@ -85,49 +57,10 @@ const empty = all.length === 0
.btn.primary { background: var(--ink); color: var(--paper); }
.btn.primary:hover { background: #000; }
- .empty-wall {
- text-align: center; background: #fff; border: 1.5px dashed var(--line);
- border-radius: var(--radius); padding: 44px 26px;
- }
- .empty-wall svg { color: color-mix(in srgb, var(--accent) 70%, var(--ink)); margin-bottom: 8px; }
- .empty-title { font-weight: 700; font-size: 18px; margin: 0 0 8px; color: var(--ink); }
- .empty-wall p { color: var(--ink-soft); max-width: 46ch; margin: 0 auto 20px; text-wrap: pretty; }
-
- .row { display: flex; flex-wrap: wrap; gap: 12px; }
- .chip {
- display: flex; align-items: center; justify-content: center; gap: 9px;
- height: 60px; padding: 0 20px;
- border: 1px solid var(--line); border-radius: 12px; background: #fff;
- text-decoration: none;
- transition: border-color 140ms ease, transform 140ms ease, box-shadow 140ms ease;
- }
- .chip:hover {
- transform: translateY(-2px); text-decoration: none;
- border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
- box-shadow: 0 12px 26px -18px rgba(28, 27, 24, 0.5);
- }
- .chip .logo { max-height: 34px; max-width: 140px; object-fit: contain; display: block; }
- .chip .avatar { width: 28px; height: 28px; border-radius: 50%; display: block; }
- .chip-name { font-weight: 700; font-size: 14.5px; color: var(--ink); letter-spacing: -0.01em; }
- .your-slot {
- border: 1.5px dashed color-mix(in srgb, var(--accent) 45%, transparent);
- color: color-mix(in srgb, var(--accent) 75%, var(--ink));
- background: color-mix(in srgb, var(--accent) 4%, transparent);
- font-weight: 600; font-size: 13.5px;
- }
- .your-slot:hover {
- background: color-mix(in srgb, var(--accent) 9%, transparent);
- border-color: color-mix(in srgb, var(--accent) 70%, transparent);
- }
-
.fine-print { font-size: 13.5px; color: var(--ink-soft); margin-top: 18px; max-width: 72ch; }
@media (max-width: 460px) {
.actions { display: grid; grid-template-columns: 1fr; gap: 10px; }
.btn { text-align: center; }
}
- @media (prefers-reduced-motion: reduce) {
- .chip { transition: none; }
- .chip:hover { transform: none; }
- }
From c73cb3bd44ed6ee5b71999a02ff04bcef3a72c5e Mon Sep 17 00:00:00 2001
From: Naman
Date: Sun, 2 Aug 2026 20:05:37 +0200
Subject: [PATCH 08/12] Final sponsor-wall design: homepage teaser links to
/sponsors, honest copy
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The homepage section is now a teaser for the dedicated page: top 8
sponsors by monthly amount (ties broken by seniority), a compact
empty-state line, and every link — the your-logo slot, '+N more', the
CTA — going to /sponsors. The dedicated page shows the full wall and
sends its CTAs straight to GitHub Sponsors checkout.
Dropped the 'founding sponsors stay marked for life' promise: it made a
lifetime placement obligation out of any one-off donation and
contradicted the sync design. The wall now promises exactly what it
does — mirror GitHub Sponsors daily, on the wall while active, roll off
when lapsed, one-time donations age out when GitHub stops counting them.
Long sponsor names truncate with an ellipsis so a single entry can't
break the row.
Co-Authored-By: Claude Fable 5
---
apps/website/src/components/SponsorWall.astro | 74 ++++++++++++-------
apps/website/src/components/Sponsors.astro | 13 ++--
2 files changed, 55 insertions(+), 32 deletions(-)
diff --git a/apps/website/src/components/SponsorWall.astro b/apps/website/src/components/SponsorWall.astro
index e5a64e2..406ddd3 100644
--- a/apps/website/src/components/SponsorWall.astro
+++ b/apps/website/src/components/SponsorWall.astro
@@ -1,14 +1,21 @@
---
-// The sponsor wall — the one component both the homepage and /sponsors render.
+// The sponsor wall — one component, two placements:
//
-// Empty, it's the pitch: a row of ghost logo slots with a live "your logo
-// here" slot, then the founding-sponsor invite and a CTA. Populated, it's a
-// flat row of avatar chips (synced daily from GitHub Sponsors) with the
-// dashed slot as the standing invite; pass `cap` to bound the row and roll
-// the rest into "+N more on GitHub →".
+// homepage teaser: top sponsors, every
+// link goes to /sponsors (the pitch page)
+// /sponsors: the full wall, links go
+// straight to GitHub Sponsors checkout
+//
+// The wall simply mirrors GitHub Sponsors: active sponsors are on it, lapsed
+// ones roll off at the next daily sync, one-time donations age out when
+// GitHub stops counting them as active. No hand-kept lists, no lifetime
+// placement promises — the copy only claims what the sync actually delivers.
import { SPONSOR_URL, sponsors } from '../lib/sponsors.js'
-const { cap } = Astro.props
+const { cap, compact = false } = Astro.props
+const href = compact ? '/sponsors/' : SPONSOR_URL
+const rel = compact ? undefined : 'noopener'
+
const all = sponsors()
const shown = cap ? all.slice(0, cap) : all
const overflow = all.length - shown.length
@@ -21,7 +28,7 @@ const ghosts = [0.72, 0.52, 0.36, 0.24, 0.15]
{empty ? (
-
+
Your logo here
@@ -29,16 +36,23 @@ const ghosts = [0.72, 0.52, 0.36, 0.24, 0.15]
))}
-
-
-
Nobody's here yet.
-
- The first sponsors get the one thing money usually can't buy on a
- sponsor wall: being first. Founding sponsors stay marked as such for
- the life of the project.
+ {compact ? (
+
+ Sponsor Quickdraw today and you're on this wall tomorrow — and on
+ the homepage, in front of every developer sizing up a whiteboard
+ SDK. An empty wall means the first logos get all the attention.
+
)}
From 9c099599789b9ec777561f1c00606590a743d8ee Mon Sep 17 00:00:00 2001
From: Naman
Date: Sun, 2 Aug 2026 22:01:25 +0200
Subject: [PATCH 09/12] Center the sponsor wall on both pages; trim the
empty-state copy
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The wall read awkwardly: a left-ragged heading and a hard-left ghost row
above a centered pitch. Both pages now center on one axis — the empty
state fades ghost slots out symmetrically from the live 'your logo here'
slot in the middle, the populated row centers too, and the surrounding
headings and copy follow (the /sponsors page reads as a pitch page, not
a doc).
The homepage now renders the same full pitch card as /sponsors instead
of its own compact one-liner; only the links differ (homepage sends
people to /sponsors, /sponsors sends them to GitHub checkout). Dropped
'An empty wall means the first logos get all the attention.'
A dead avatar URL now removes itself rather than leaving a broken glyph
— the name alone still reads as a finished chip.
Co-Authored-By: Claude Opus 5
---
apps/website/src/components/SponsorWall.astro | 105 +++++++++---------
apps/website/src/components/Sponsors.astro | 23 ++--
apps/website/src/pages/sponsors.astro | 14 ++-
3 files changed, 75 insertions(+), 67 deletions(-)
diff --git a/apps/website/src/components/SponsorWall.astro b/apps/website/src/components/SponsorWall.astro
index 406ddd3..f0aef03 100644
--- a/apps/website/src/components/SponsorWall.astro
+++ b/apps/website/src/components/SponsorWall.astro
@@ -1,10 +1,15 @@
---
// The sponsor wall — one component, two placements:
//
-// homepage teaser: top sponsors, every
-// link goes to /sponsors (the pitch page)
-// /sponsors: the full wall, links go
-// straight to GitHub Sponsors checkout
+// homepage: top sponsors, every link goes
+// to /sponsors (the pitch page)
+// /sponsors: the full wall, links go
+// straight to GitHub Sponsors checkout
+//
+// Everything is centered on the card's axis: the empty state fades ghost
+// slots out symmetrically from the live "your logo here" slot in the middle,
+// and the populated row centers too, so a half-full row never reads as
+// left-over space.
//
// The wall simply mirrors GitHub Sponsors: active sponsors are on it, lapsed
// ones roll off at the next daily sync, one-time donations age out when
@@ -12,47 +17,39 @@
// placement promises — the copy only claims what the sync actually delivers.
import { SPONSOR_URL, sponsors } from '../lib/sponsors.js'
-const { cap, compact = false } = Astro.props
-const href = compact ? '/sponsors/' : SPONSOR_URL
-const rel = compact ? undefined : 'noopener'
+const { cap, toPage = false } = Astro.props
+const href = toPage ? '/sponsors/' : SPONSOR_URL
+const rel = toPage ? undefined : 'noopener'
const all = sponsors()
const shown = cap ? all.slice(0, cap) : all
const overflow = all.length - shown.length
const empty = all.length === 0
-// Ghost slots fade out to the right, so the space reads as "waiting for
-// logos", not "broken".
-const ghosts = [0.72, 0.52, 0.36, 0.24, 0.15]
+// Ghosts fade away from the live slot in both directions, so the invite sits
+// at the optical center of the row.
+const ghostsLeft = [0.2, 0.45]
+const ghostsRight = [0.45, 0.2]
---
{empty ? (
-
+
+ {ghostsLeft.map((o) => )}
-
+
Your logo here
- {ghosts.map((o) => (
-
- ))}
+ {ghostsRight.map((o) => )}
+ Sponsor Quickdraw today and you're on this wall tomorrow — in front of
+ every developer sizing up a whiteboard SDK.
- ) : (
-
-
-
Nobody's here yet.
-
- Sponsor Quickdraw today and you're on this wall tomorrow — and on
- the homepage, in front of every developer sizing up a whiteboard
- SDK. An empty wall means the first logos get all the attention.
-