diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..822e4d5 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# Shows the "Sponsor" button on the repo page. +github: quickdrawjs +custom: ['https://tryquickdraw.com/sponsors/'] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 454fa9e..70a93db 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: true contact_links: - name: Question or idea - url: https://github.com/nmndwivedi/quickdraw/discussions + url: https://github.com/quickdrawjs/quickdraw/discussions about: Not a bug or a concrete feature request? Start a discussion. - name: Security issue - url: https://github.com/nmndwivedi/quickdraw/security/advisories/new + url: https://github.com/quickdrawjs/quickdraw/security/advisories/new about: Please report vulnerabilities privately, not as public issues. diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml new file mode 100644 index 0000000..433caed --- /dev/null +++ b/.github/workflows/sponsors.yml @@ -0,0 +1,55 @@ +name: Sponsors + +# Keeps docs/sponsors.json current so the website's sponsor wall features +# sponsors automatically — new sponsors appear within a day of sponsoring, +# lapsed ones roll off, no manual list edits. +# +# The Sponsors GraphQL API rejects anonymous reads, so this runs here with a +# token. If the default Actions token can't read the org's sponsorships, add +# a classic PAT with read:org as the SPONSORS_TOKEN secret — it's preferred +# automatically when set. + +on: + schedule: + - cron: '43 4 * * *' # daily, 04:43 UTC + workflow_dispatch: # refresh on demand + push: + branches: [main] + paths: + - 'scripts/sponsors.mjs' + - '.github/workflows/sponsors.yml' + +permissions: + contents: write + +concurrency: + group: sponsors + cancel-in-progress: false + +jobs: + update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Refresh sponsors snapshot + env: + GITHUB_TOKEN: ${{ secrets.SPONSORS_TOKEN || secrets.GITHUB_TOKEN }} + run: node scripts/sponsors.mjs + + - name: Commit if the wall changed + run: | + # Stage first: on the very first run the file is untracked, and + # `git diff` alone would report no change and skip the seed commit. + git add -A docs/sponsors.json + if git diff --cached --quiet; then + echo "No change." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "chore: update sponsors [skip ci]" + git push 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..dfa122b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Quickdraw -[![CI](https://github.com/nmndwivedi/quickdraw/actions/workflows/ci.yml/badge.svg)](https://github.com/nmndwivedi/quickdraw/actions/workflows/ci.yml) +[![CI](https://github.com/quickdrawjs/quickdraw/actions/workflows/ci.yml/badge.svg)](https://github.com/quickdrawjs/quickdraw/actions/workflows/ci.yml) [![npm: @quickdrawjs/core](https://img.shields.io/npm/v/@quickdrawjs/core?label=%40quickdrawjs%2Fcore&logo=npm)](https://www.npmjs.com/package/@quickdrawjs/core) [![npm: @quickdrawjs/react](https://img.shields.io/npm/v/@quickdrawjs/react?label=%40quickdrawjs%2Freact&logo=npm)](https://www.npmjs.com/package/@quickdrawjs/react) [![npm: @quickdrawjs/react-native](https://img.shields.io/npm/v/@quickdrawjs/react-native?label=%40quickdrawjs%2Freact-native&logo=npm)](https://www.npmjs.com/package/@quickdrawjs/react-native) @@ -217,15 +217,34 @@ npm run typecheck # validate the published type declarations Quickdraw is young. What it doesn't have yet: a first-party multiplayer server (the diff-based store is sync-ready — you bring the transport), layers, frames, or rich text. The full roadmap lives in -[issue #1](https://github.com/nmndwivedi/quickdraw/issues/1) — comment there +[issue #1](https://github.com/quickdrawjs/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://github.com/sponsors/quickdrawjs) +are what keep it that way. Sponsors get their logo on +[the website](https://tryquickdraw.com/sponsors/) and here: + +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 star is how other developers find it. - + @@ -241,8 +260,8 @@ same data as the chart on the websit Quickdraw is open to contributions from everyone — bug reports, features, docs, examples. Start with [CONTRIBUTING.md](CONTRIBUTING.md), browse -[`good first issue`](https://github.com/nmndwivedi/quickdraw/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), -or open a [discussion](https://github.com/nmndwivedi/quickdraw/discussions) +[`good first issue`](https://github.com/quickdrawjs/quickdraw/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), +or open a [discussion](https://github.com/quickdrawjs/quickdraw/discussions) if you're not sure where a change belongs. We follow the [Contributor Covenant](CODE_OF_CONDUCT.md). diff --git a/SECURITY.md b/SECURITY.md index 0c03883..a5f40dc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,7 +10,7 @@ security fixes. Please **do not open a public issue** for security problems. Report privately via -[GitHub Security Advisories](https://github.com/nmndwivedi/quickdraw/security/advisories/new) +[GitHub Security Advisories](https://github.com/quickdrawjs/quickdraw/security/advisories/new) or email **promptifyapps@gmail.com**. You can expect an acknowledgment within a few days. Once a fix ships we'll diff --git a/apps/app/index.html b/apps/app/index.html index 2f6c7ee..ae4e9c3 100644 --- a/apps/app/index.html +++ b/apps/app/index.html @@ -16,58 +16,96 @@ diff --git a/apps/website/src/components/Sponsors.astro b/apps/website/src/components/Sponsors.astro new file mode 100644 index 0000000..a38f635 --- /dev/null +++ b/apps/website/src/components/Sponsors.astro @@ -0,0 +1,55 @@ +--- +// Homepage sponsors section — the top sponsors, with the rest behind the +// wall's own "+N more →" link to /sponsors. Every call to action goes +// straight to GitHub Sponsors. The wall carries its own CTA when empty, 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 hasSponsors = sponsors().length > 0 +--- +
+

Sponsors keep it MIT

+

+ Quickdraw has no license fees and never will — sponsors are what make that + math work. Every sponsor gets a spot on this wall. +

+ + + + {hasSponsors && ( +
+ )} +
+ + diff --git a/apps/website/src/consts.js b/apps/website/src/consts.js index d9144ff..b963fa5 100644 --- a/apps/website/src/consts.js +++ b/apps/website/src/consts.js @@ -2,6 +2,6 @@ // and app.tryquickdraw.com resolves, point APP_URL there. export const SITE_URL = 'https://tryquickdraw.com' export const APP_URL = 'https://app.tryquickdraw.com' -export const GITHUB_URL = 'https://github.com/nmndwivedi/quickdraw' +export const GITHUB_URL = 'https://github.com/quickdrawjs/quickdraw' export const SITE_NAME = 'Quickdraw' export const SITE_TAGLINE = 'The MIT-licensed infinite-canvas whiteboard SDK' 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 Issues Discussions Changelog + Sponsors
Packages diff --git a/apps/website/src/lib/liveStars.js b/apps/website/src/lib/liveStars.js index 19ca6a8..1f56d1c 100644 --- a/apps/website/src/lib/liveStars.js +++ b/apps/website/src/lib/liveStars.js @@ -7,7 +7,7 @@ // // Both fail soft — the page already shipped with build-time values baked in. -const REPO = 'nmndwivedi/quickdraw' +const REPO = 'quickdrawjs/quickdraw' const REPO_API = `https://api.github.com/repos/${REPO}` const SNAPSHOT = `https://raw.githubusercontent.com/${REPO}/main/docs/star-history.json` const COUNT_KEY = 'qd-star-count' diff --git a/apps/website/src/lib/sponsors.js b/apps/website/src/lib/sponsors.js new file mode 100644 index 0000000..d4cd560 --- /dev/null +++ b/apps/website/src/lib/sponsors.js @@ -0,0 +1,41 @@ +// The sponsor wall's data source — deliberately simple: no tiers, no prices. +// +// The wall feeds itself: .github/workflows/sponsors.yml refreshes +// docs/sponsors.json from the GitHub Sponsors API daily, and this module +// reads that snapshot at build time. New sponsors appear automatically with +// their GitHub avatar and name; lapsed ones roll off. +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +export const SPONSOR_URL = 'https://github.com/sponsors/quickdrawjs' + +// Hand overrides keyed by GitHub login — e.g. a sponsor who sends a proper +// SVG instead of their avatar: 'acme': { logo: '/sponsors/acme.svg' } +export const OVERRIDES = {} + +function readSnapshot() { + // Resolved from this file so it doesn't matter where the build is invoked. + const path = fileURLToPath(new URL('../../../../docs/sponsors.json', import.meta.url)) + try { + const data = JSON.parse(readFileSync(path, 'utf8')) + return Array.isArray(data.sponsors) ? data.sponsors : [] + } catch { + // No snapshot yet (fresh clone, or the workflow hasn't run) — the wall + // just shows its empty state. + return [] + } +} + +// One flat list, most generous first so bigger supporters sit up front. +export function sponsors() { + return readSnapshot() + .slice() + .sort((a, b) => (b.monthly ?? 0) - (a.monthly ?? 0) || (a.since ?? '').localeCompare(b.since ?? '')) + .map((s) => ({ + name: s.name || s.login, + url: s.url || `https://github.com/${s.login}`, + avatar: s.avatar, + logo: null, + ...OVERRIDES[s.login], + })) +} diff --git a/apps/website/src/pages/blog/add-whiteboard-to-react-app.md b/apps/website/src/pages/blog/add-whiteboard-to-react-app.md index e51ac91..34981a6 100644 --- a/apps/website/src/pages/blog/add-whiteboard-to-react-app.md +++ b/apps/website/src/pages/blog/add-whiteboard-to-react-app.md @@ -124,4 +124,4 @@ deeper story — why diffs beat operational transforms for this use case — rea - [Documentation](/docs/) — persistence, headless mode, React Native - [Try the hosted app](https://app.tryquickdraw.com) — the same SDK, deployed -- [GitHub](https://github.com/nmndwivedi/quickdraw) — MIT, open to contributions +- [GitHub](https://github.com/quickdrawjs/quickdraw) — MIT, open to contributions diff --git a/apps/website/src/pages/blog/free-online-whiteboard-no-signup.md b/apps/website/src/pages/blog/free-online-whiteboard-no-signup.md index 9ffdf83..145e6d9 100644 --- a/apps/website/src/pages/blog/free-online-whiteboard-no-signup.md +++ b/apps/website/src/pages/blog/free-online-whiteboard-no-signup.md @@ -53,7 +53,7 @@ npm install @quickdrawjs/react ``` The [docs](/docs/) take it from there, and the whole thing is -[open source](https://github.com/nmndwivedi/quickdraw). +[open source](https://github.com/quickdrawjs/quickdraw). ## Keyboard shortcuts worth knowing diff --git a/apps/website/src/pages/blog/infinite-canvas-camera-math.md b/apps/website/src/pages/blog/infinite-canvas-camera-math.md index 9bf71be..a283bf2 100644 --- a/apps/website/src/pages/blog/infinite-canvas-camera-math.md +++ b/apps/website/src/pages/blog/infinite-canvas-camera-math.md @@ -117,7 +117,7 @@ working when zoomed" bugs. ## See it running All of this is implemented in -[Quickdraw's editor](https://github.com/nmndwivedi/quickdraw), MIT-licensed +[Quickdraw's editor](https://github.com/quickdrawjs/quickdraw), MIT-licensed — `editor.js` has the camera, `shapes.js` the hit-testing. If you'd rather not build the rest of the whiteboard (undo, selection, styles, export...), that's the [point of the SDK](/docs/): diff --git a/apps/website/src/pages/blog/introducing-quickdraw.md b/apps/website/src/pages/blog/introducing-quickdraw.md index 1fea94c..ee8c589 100644 --- a/apps/website/src/pages/blog/introducing-quickdraw.md +++ b/apps/website/src/pages/blog/introducing-quickdraw.md @@ -137,18 +137,18 @@ the transport), no layers, no frames, and no rich text. Excalidraw and tldraw have years of production hardening and far bigger communities than a project released today. -The [roadmap is public](https://github.com/nmndwivedi/quickdraw/issues/1). +The [roadmap is public](https://github.com/quickdrawjs/quickdraw/issues/1). If the missing feature that sent you back to a paid SDK is on it — or isn't — say so there. ## Try it - **Live app, no signup:** [app.tryquickdraw.com](https://app.tryquickdraw.com) -- **Repo:** [github.com/nmndwivedi/quickdraw](https://github.com/nmndwivedi/quickdraw) +- **Repo:** [github.com/quickdrawjs/quickdraw](https://github.com/quickdrawjs/quickdraw) - **Docs:** [tryquickdraw.com](https://tryquickdraw.com) If you've embedded a whiteboard in production before, I'd genuinely like to know what you hit that I should handle better — -[open an issue](https://github.com/nmndwivedi/quickdraw/issues) or start a -[discussion](https://github.com/nmndwivedi/quickdraw/discussions). And if +[open an issue](https://github.com/quickdrawjs/quickdraw/issues) or start a +[discussion](https://github.com/quickdrawjs/quickdraw/discussions). And if it's useful to you, a star helps other people find it. diff --git a/apps/website/src/pages/blog/open-source-tldraw-alternatives.md b/apps/website/src/pages/blog/open-source-tldraw-alternatives.md index 8c42994..658812e 100644 --- a/apps/website/src/pages/blog/open-source-tldraw-alternatives.md +++ b/apps/website/src/pages/blog/open-source-tldraw-alternatives.md @@ -108,7 +108,7 @@ not a knock on either project. - **You only need ink and enjoy building canvases:** perfect-freehand. - **The license cost is fine for your business:** tldraw remains excellent. -Quickdraw is [open to contributions](https://github.com/nmndwivedi/quickdraw). +Quickdraw is [open to contributions](https://github.com/quickdrawjs/quickdraw). If the missing feature that sent you back to a paid SDK is on the -[roadmap](https://github.com/nmndwivedi/quickdraw/issues/1), tell us — or +[roadmap](https://github.com/quickdrawjs/quickdraw/issues/1), tell us — or build it with us. diff --git a/apps/website/src/pages/blog/react-native-whiteboard-drawing.md b/apps/website/src/pages/blog/react-native-whiteboard-drawing.md index ff1a68d..33badf8 100644 --- a/apps/website/src/pages/blog/react-native-whiteboard-drawing.md +++ b/apps/website/src/pages/blog/react-native-whiteboard-drawing.md @@ -96,4 +96,4 @@ renderer (`@shopify/react-native-skia`) is the right tool — at the cost of building the whiteboard layer yourself. For a document-style whiteboard, notes app, or annotation feature, the WebView architecture is what ships. -*Quickdraw is MIT-licensed and [open to contributions](https://github.com/nmndwivedi/quickdraw).* +*Quickdraw is MIT-licensed and [open to contributions](https://github.com/quickdrawjs/quickdraw).* diff --git a/apps/website/src/pages/blog/real-time-whiteboard-sync-json-diffs.md b/apps/website/src/pages/blog/real-time-whiteboard-sync-json-diffs.md index d35bb61..fbb2a33 100644 --- a/apps/website/src/pages/blog/real-time-whiteboard-sync-json-diffs.md +++ b/apps/website/src/pages/blog/real-time-whiteboard-sync-json-diffs.md @@ -102,8 +102,8 @@ audit trail of a board, stroke by stroke. ## Try it -The [roadmap](https://github.com/nmndwivedi/quickdraw/issues/1) includes a +The [roadmap](https://github.com/quickdrawjs/quickdraw/issues/1) includes a runnable two-browser sync example, and -[issue #4](https://github.com/nmndwivedi/quickdraw/issues/4) is open if you'd +[issue #4](https://github.com/quickdrawjs/quickdraw/issues/4) is open if you'd like to build it with us. Quickdraw is MIT-licensed: -[github.com/nmndwivedi/quickdraw](https://github.com/nmndwivedi/quickdraw). +[github.com/quickdrawjs/quickdraw](https://github.com/quickdrawjs/quickdraw). diff --git a/apps/website/src/pages/blog/tldraw-vs-excalidraw-vs-quickdraw.md b/apps/website/src/pages/blog/tldraw-vs-excalidraw-vs-quickdraw.md index 5644b8e..15bbd04 100644 --- a/apps/website/src/pages/blog/tldraw-vs-excalidraw-vs-quickdraw.md +++ b/apps/website/src/pages/blog/tldraw-vs-excalidraw-vs-quickdraw.md @@ -71,7 +71,7 @@ The flip side of weight is capability: tldraw's shape and tool plugin system is the richest of the three, and Excalidraw ships years of accumulated features (component libraries, rich export options, localization). Quickdraw ships a complete but focused whiteboard; its -[roadmap](https://github.com/nmndwivedi/quickdraw/issues/1) is public. +[roadmap](https://github.com/quickdrawjs/quickdraw/issues/1) is public. ## Look and feel @@ -97,7 +97,7 @@ board switches between light and dark themes. Be honest about this one: tldraw and Excalidraw have big communities, years of production hardening, and large test surfaces. Quickdraw is new — smaller API surface, ~120 tests, one engine shared across three packages, -and an open [issue tracker](https://github.com/nmndwivedi/quickdraw/issues) +and an open [issue tracker](https://github.com/quickdrawjs/quickdraw/issues) you can actually influence. Young projects earn trust in public; that's why everything is MIT on GitHub. @@ -113,4 +113,4 @@ everything is MIT on GitHub. Corrections welcome — licensing and features change, and we'd rather this page be right than flattering: -[open an issue](https://github.com/nmndwivedi/quickdraw/issues). +[open an issue](https://github.com/quickdrawjs/quickdraw/issues). 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..081df80 --- /dev/null +++ b/apps/website/src/pages/sponsors.astro @@ -0,0 +1,59 @@ +--- +// 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 } from '../lib/sponsors.js' +--- + +
+
+

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. +

+ +
+ +
+

The wall

+ +
+
+ + + diff --git a/docs/star-history-dark.svg b/docs/star-history-dark.svg index 7b9a088..5ef14ed 100644 --- a/docs/star-history-dark.svg +++ b/docs/star-history-dark.svg @@ -9,7 +9,7 @@ 20 stars - nmndwivedi/quickdraw + quickdrawjs/quickdraw diff --git a/docs/star-history.json b/docs/star-history.json index 439a0c9..9e4f955 100644 --- a/docs/star-history.json +++ b/docs/star-history.json @@ -1,5 +1,5 @@ { - "repo": "nmndwivedi/quickdraw", + "repo": "quickdrawjs/quickdraw", "stars": 20, "createdAt": "2026-07-31T22:22:15Z", "generatedAt": "2026-08-02T12:23:29.848Z", diff --git a/docs/star-history.svg b/docs/star-history.svg index c90d649..e8b1d1f 100644 --- a/docs/star-history.svg +++ b/docs/star-history.svg @@ -9,7 +9,7 @@ 20 stars - nmndwivedi/quickdraw + quickdrawjs/quickdraw diff --git a/packages/core/README.md b/packages/core/README.md index 49ec993..edfdab1 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -107,7 +107,7 @@ Remote diffs don't enter local undo history, so collaborative undo stays sane. const blob = await editor.exportImage({ background: true, scale: 2 }) ``` -See the [repository README](https://github.com/nmndwivedi/quickdraw) for the +See the [repository README](https://github.com/quickdrawjs/quickdraw) for the full API, data model and guides. ## License diff --git a/packages/core/package.json b/packages/core/package.json index 2f03b2e..9d68343 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -38,10 +38,10 @@ "homepage": "https://tryquickdraw.com", "repository": { "type": "git", - "url": "https://github.com/nmndwivedi/quickdraw.git", + "url": "https://github.com/quickdrawjs/quickdraw.git", "directory": "packages/core" }, - "bugs": "https://github.com/nmndwivedi/quickdraw/issues", + "bugs": "https://github.com/quickdrawjs/quickdraw/issues", "publishConfig": { "access": "public" } 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..6c4b93c 100644 --- a/packages/core/src/quickdraw.css +++ b/packages/core/src/quickdraw.css @@ -1,5 +1,8 @@ /* Quickdraw styles — self-contained, no preprocessor, importable raw. - The dock is dark glass so it reads on either theme's paper. */ + All floating chrome (dock, actions, popovers) follows the board's theme + through the --qd-* custom properties below: dark glass on the dark paper, + light glass on the light one. Theme switches animate because the surfaces + transition background/color/border while the variables flip. */ .qd-root { position: relative; @@ -10,10 +13,65 @@ background: #fbf9f4; touch-action: none; border-radius: inherit; + transition: background 320ms ease; /* lets the watermark dodge the dock on narrow boards (see below) */ container-type: inline-size; + + /* light-theme chrome */ + --qd-bar-bg: rgba(255, 255, 255, 0.88); + --qd-pop-bg: rgba(255, 255, 255, 0.94); + --qd-border: rgba(28, 27, 24, 0.12); + --qd-bar-shadow: 0 4px 16px rgba(28, 27, 24, 0.14); + --qd-pop-shadow: 0 8px 28px rgba(28, 27, 24, 0.2); + --qd-ink: rgba(28, 27, 24, 0.82); + --qd-ink-strong: rgba(28, 27, 24, 0.94); + --qd-ink-soft: rgba(28, 27, 24, 0.55); + --qd-ink-faint: rgba(28, 27, 24, 0.4); + --qd-hover: rgba(28, 27, 24, 0.07); + --qd-hover-strong: rgba(28, 27, 24, 0.1); + --qd-active: rgba(28, 27, 24, 0.12); + --qd-on-bg: #211d14; + --qd-on-ink: #fbf9f4; + --qd-divider: rgba(28, 27, 24, 0.14); + --qd-menu-div: rgba(28, 27, 24, 0.1); + --qd-seg-bg: rgba(28, 27, 24, 0.07); + --qd-dot-ring: rgba(28, 27, 24, 0.12); +} +.qd-root[data-qd-theme='dark'] { + background: #191713; + + /* dark-theme chrome: the original dark glass */ + --qd-bar-bg: rgba(19, 17, 13, 0.72); + --qd-pop-bg: rgba(19, 17, 13, 0.85); + --qd-border: rgba(255, 255, 255, 0.14); + --qd-bar-shadow: 0 4px 16px rgba(10, 8, 4, 0.25); + --qd-pop-shadow: 0 8px 28px rgba(10, 8, 4, 0.35); + --qd-ink: rgba(255, 255, 255, 0.85); + --qd-ink-strong: rgba(255, 255, 255, 0.95); + --qd-ink-soft: rgba(255, 255, 255, 0.62); + --qd-ink-faint: rgba(255, 255, 255, 0.4); + --qd-hover: rgba(255, 255, 255, 0.1); + --qd-hover-strong: rgba(255, 255, 255, 0.12); + --qd-active: rgba(255, 255, 255, 0.22); + --qd-on-bg: rgba(255, 255, 255, 0.92); + --qd-on-ink: #211d14; + --qd-divider: rgba(255, 255, 255, 0.16); + --qd-menu-div: rgba(255, 255, 255, 0.1); + --qd-seg-bg: rgba(255, 255, 255, 0.08); + --qd-dot-ring: rgba(255, 255, 255, 0.12); +} + +/* 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-root[data-qd-theme='dark'] { background: #191713; } .qd-canvas, .qd-overlay { position: absolute; @@ -24,6 +82,8 @@ .qd-overlay { pointer-events: none; } /* ---- floating chrome ---- */ +/* The theme switch rides these transitions; hovers use the same clock, which + at 140–320ms still reads as instant. */ .qd-ui { position: absolute; inset: 0; pointer-events: none; z-index: 40; } .qd-ui.qd-hidden { display: none; } .qd-ui button { -webkit-tap-highlight-color: transparent; } @@ -39,12 +99,13 @@ gap: 2px; padding: 5px 7px; border-radius: 999px; - background: rgba(19, 17, 13, 0.72); - border: 1px solid rgba(255, 255, 255, 0.14); - box-shadow: 0 4px 16px rgba(10, 8, 4, 0.25); + background: var(--qd-bar-bg); + border: 1px solid var(--qd-border); + box-shadow: var(--qd-bar-shadow); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); pointer-events: auto; + transition: background 320ms ease, border-color 320ms ease, box-shadow 320ms ease; } /* history + selection actions: a vertical pill hugging the left edge. @@ -60,16 +121,17 @@ gap: 2px; padding: 6px 4px; border-radius: 999px; - background: rgba(19, 17, 13, 0.72); - border: 1px solid rgba(255, 255, 255, 0.14); - box-shadow: 0 4px 16px rgba(10, 8, 4, 0.25); + background: var(--qd-bar-bg); + border: 1px solid var(--qd-border); + box-shadow: var(--qd-bar-shadow); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); pointer-events: auto; + transition: background 320ms ease, border-color 320ms ease, box-shadow 320ms ease; } .qd-actions button svg { width: 17px; height: 17px; display: block; } .qd-actions .qd-div { width: 18px; height: 1px; margin: 3px 0; } -.qd-actions button:hover { background: rgba(255, 255, 255, 0.1); } +.qd-actions button:hover { background: var(--qd-hover); } .qd-actions button:disabled { opacity: 0.32; pointer-events: none; } .qd-actions .qd-tool { width: 30px; height: 30px; } @@ -83,15 +145,16 @@ border: none; border-radius: 9px; background: transparent; - color: rgba(255, 255, 255, 0.85); + color: var(--qd-ink); cursor: pointer; padding: 0; + transition: background 140ms ease, color 140ms ease; } .qd-dock button svg { width: 19px; height: 19px; display: block; } -.qd-dock button:hover { background: rgba(255, 255, 255, 0.1); } -.qd-dock button.on { background: rgba(255, 255, 255, 0.92); color: #211d14; } +.qd-dock button:hover { background: var(--qd-hover); } +.qd-dock button.on { background: var(--qd-on-bg); color: var(--qd-on-ink); } .qd-dock button:disabled { opacity: 0.32; pointer-events: none; } -.qd-div { width: 1px; height: 20px; margin: 0 3px; background: rgba(255, 255, 255, 0.16); flex: none; } +.qd-div { width: 1px; height: 20px; margin: 0 3px; background: var(--qd-divider); flex: none; transition: background 320ms ease; } .qd-dock.qd-compact { gap: 2px; } .qd-geo-btn { position: relative; } @@ -112,10 +175,11 @@ width: 16px; height: 16px; border-radius: 50%; - border: 2.5px solid rgba(255, 255, 255, 0.92); + border: 2.5px solid var(--qd-on-bg); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2); + transition: border-color 320ms ease; } -.qd-style-btn.on .qd-style-dot { border-color: #211d14; } +.qd-style-btn.on .qd-style-dot { border-color: var(--qd-on-ink); } /* ---- popovers ---- */ .qd-popover { @@ -123,15 +187,16 @@ bottom: 54px; left: 50%; border-radius: 13px; - background: rgba(19, 17, 13, 0.85); - border: 1px solid rgba(255, 255, 255, 0.14); - box-shadow: 0 8px 28px rgba(10, 8, 4, 0.35); + background: var(--qd-pop-bg); + border: 1px solid var(--qd-border); + box-shadow: var(--qd-pop-shadow); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); pointer-events: auto; padding: 8px; max-width: calc(100% - 16px); box-sizing: border-box; + transition: background 320ms ease, border-color 320ms ease, box-shadow 320ms ease; /* rise out of the dock: a short fade + lift + settle */ transform-origin: 50% 100%; animation: qd-pop-in 160ms cubic-bezier(0.2, 0.9, 0.3, 1.15); @@ -149,16 +214,16 @@ .qd-popover .qd-tool svg { width: 19px; height: 19px; display: block; } .qd-geo-pop { display: flex; gap: 2px; flex-wrap: wrap; } -.qd-geo-pop .qd-tool { color: rgba(255, 255, 255, 0.85); } +.qd-geo-pop .qd-tool { color: var(--qd-ink); } /* overflow tools ("more" flyout / folded kit) */ .qd-grid-pop { display: grid; grid-template-columns: repeat(4, 32px); gap: 2px; } -.qd-grid-pop .qd-tool { color: rgba(255, 255, 255, 0.85); } -.qd-grid-pop .qd-tool.on { background: rgba(255, 255, 255, 0.92); color: #211d14; } +.qd-grid-pop .qd-tool { color: var(--qd-ink); } +.qd-grid-pop .qd-tool.on { background: var(--qd-on-bg); color: var(--qd-on-ink); } .qd-style-pop { width: 196px; } .qd-row { display: flex; align-items: center; justify-content: space-between; } -.qd-row + .qd-row { margin-top: 7px; padding-top: 7px; border-top: 1px solid rgba(255, 255, 255, 0.1); } +.qd-row + .qd-row { margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--qd-menu-div); } .qd-colors { display: grid; grid-template-columns: repeat(6, 1fr); gap: 3px; } .qd-dot { width: 26px; @@ -171,6 +236,7 @@ justify-content: center; cursor: pointer; padding: 0; + transition: background 140ms ease; } .qd-dot::before { content: ''; @@ -178,10 +244,10 @@ height: 15px; border-radius: 50%; background: var(--dot); - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12); + box-shadow: 0 0 0 1px var(--qd-dot-ring); } -.qd-dot:hover { background: rgba(255, 255, 255, 0.1); } -.qd-dot.on { background: rgba(255, 255, 255, 0.22); } +.qd-dot:hover { background: var(--qd-hover); } +.qd-dot.on { background: var(--qd-active); } .qd-opt { width: 40px; @@ -189,16 +255,17 @@ border: none; border-radius: 8px; background: transparent; - color: rgba(255, 255, 255, 0.85); + color: var(--qd-ink); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; + transition: background 140ms ease, color 140ms ease; } .qd-opt svg { width: 20px; height: 20px; } -.qd-opt:hover { background: rgba(255, 255, 255, 0.1); } -.qd-opt.on { background: rgba(255, 255, 255, 0.92); color: #211d14; } +.qd-opt:hover { background: var(--qd-hover); } +.qd-opt.on { background: var(--qd-on-bg); color: var(--qd-on-ink); } .qd-size-pip { width: var(--pip); height: var(--pip); @@ -213,26 +280,27 @@ gap: 10px; border: none; background: transparent; - color: rgba(255, 255, 255, 0.88); + color: var(--qd-ink-strong); text-align: left; font: 500 12.5px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 7px 10px; border-radius: 8px; cursor: pointer; + transition: background 140ms ease, color 140ms ease; } -.qd-menu-item:hover { background: rgba(255, 255, 255, 0.1); } -.qd-mi-ico { display: flex; flex: 0 0 16px; color: rgba(255, 255, 255, 0.62); } +.qd-menu-item:hover { background: var(--qd-hover); } +.qd-mi-ico { display: flex; flex: 0 0 16px; color: var(--qd-ink-soft); } .qd-mi-ico svg { width: 16px; height: 16px; } .qd-menu-item:hover .qd-mi-ico { color: inherit; } .qd-mi-label { flex: 1 1 auto; white-space: nowrap; } .qd-mi-key { flex: 0 0 auto; - color: rgba(255, 255, 255, 0.4); + color: var(--qd-ink-faint); font-size: 11px; letter-spacing: 0.02em; font-variant-numeric: tabular-nums; } -.qd-menu-div { display: block; height: 1px; margin: 5px 4px; background: rgba(255, 255, 255, 0.1); } +.qd-menu-div { display: block; height: 1px; margin: 5px 4px; background: var(--qd-menu-div); } /* labelled rows of mutually exclusive icon buttons (grid, theme) */ .qd-menu-row { @@ -240,7 +308,7 @@ align-items: center; gap: 10px; padding: 4px 10px 4px 10px; - color: rgba(255, 255, 255, 0.88); + color: var(--qd-ink-strong); font: 500 12.5px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } .qd-menu-row + .qd-menu-row { margin-top: 2px; } @@ -249,7 +317,8 @@ gap: 2px; padding: 2px; border-radius: 9px; - background: rgba(255, 255, 255, 0.08); + background: var(--qd-seg-bg); + transition: background 320ms ease; } .qd-seg-btn { width: 30px; @@ -257,7 +326,7 @@ border: none; border-radius: 7px; background: transparent; - color: rgba(255, 255, 255, 0.72); + color: var(--qd-ink); display: flex; align-items: center; justify-content: center; @@ -266,16 +335,16 @@ transition: background 120ms ease, color 120ms ease; } .qd-seg-btn svg { width: 16px; height: 16px; } -.qd-seg-btn:hover { background: rgba(255, 255, 255, 0.12); color: #fff; } -.qd-seg-btn.on { background: rgba(255, 255, 255, 0.92); color: #211d14; } +.qd-seg-btn:hover { background: var(--qd-hover-strong); color: var(--qd-ink-strong); } +.qd-seg-btn.on { background: var(--qd-on-bg); color: var(--qd-on-ink); } /* nested dropdown (grid backdrop): a flyout beside the parent row */ -.qd-mi-value { flex: 0 0 auto; color: rgba(255, 255, 255, 0.45); font-size: 11.5px; } -.qd-mi-chev, .qd-mi-check { flex: 0 0 14px; display: flex; color: rgba(255, 255, 255, 0.4); } +.qd-mi-value { flex: 0 0 auto; color: var(--qd-ink-faint); font-size: 11.5px; } +.qd-mi-chev, .qd-mi-check { flex: 0 0 14px; display: flex; color: var(--qd-ink-faint); } .qd-mi-chev svg, .qd-mi-check svg { width: 14px; height: 14px; } -.qd-mi-check { color: rgba(255, 255, 255, 0.85); } +.qd-mi-check { color: var(--qd-ink); } .qd-has-sub { position: relative; } -.qd-has-sub.sub-open { background: rgba(255, 255, 255, 0.1); } +.qd-has-sub.sub-open { background: var(--qd-hover); } .qd-submenu { position: absolute; left: calc(100% + 2px); @@ -285,9 +354,9 @@ min-width: 148px; padding: 6px; border-radius: 13px; - background: rgba(19, 17, 13, 0.85); - border: 1px solid rgba(255, 255, 255, 0.14); - box-shadow: 0 8px 28px rgba(10, 8, 4, 0.35); + background: var(--qd-pop-bg); + border: 1px solid var(--qd-border); + box-shadow: var(--qd-pop-shadow); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); animation: qd-sub-in 130ms ease; @@ -325,7 +394,7 @@ letter-spacing: 0.01em; text-decoration: none; -webkit-tap-highlight-color: transparent; - transition: color 140ms ease, background 140ms ease; + transition: color 320ms ease, background 140ms ease; } .qd-watermark svg { width: 14px; height: 14px; display: block; } .qd-watermark:hover { 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') diff --git a/packages/react-native/README.md b/packages/react-native/README.md index d7860af..76afc5b 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -75,7 +75,7 @@ changes stay out of local undo history. // socket.onmessage: board.current.applyDiff(JSON.parse(data)) ``` -See the [repository README](https://github.com/nmndwivedi/quickdraw) for the +See the [repository README](https://github.com/quickdrawjs/quickdraw) for the full API. ## License diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 2abc17d..616eaf4 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -52,10 +52,10 @@ "homepage": "https://tryquickdraw.com", "repository": { "type": "git", - "url": "https://github.com/nmndwivedi/quickdraw.git", + "url": "https://github.com/quickdrawjs/quickdraw.git", "directory": "packages/react-native" }, - "bugs": "https://github.com/nmndwivedi/quickdraw/issues", + "bugs": "https://github.com/quickdrawjs/quickdraw/issues", "publishConfig": { "access": "public" } diff --git a/packages/react/README.md b/packages/react/README.md index 01fb3a1..14740ad 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -93,7 +93,7 @@ useEffect(() => { ``` -See the [repository README](https://github.com/nmndwivedi/quickdraw) for the +See the [repository README](https://github.com/quickdrawjs/quickdraw) for the full API. ## License diff --git a/packages/react/package.json b/packages/react/package.json index ae3d84e..dc38d84 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -39,10 +39,10 @@ "homepage": "https://tryquickdraw.com", "repository": { "type": "git", - "url": "https://github.com/nmndwivedi/quickdraw.git", + "url": "https://github.com/quickdrawjs/quickdraw.git", "directory": "packages/react" }, - "bugs": "https://github.com/nmndwivedi/quickdraw/issues", + "bugs": "https://github.com/quickdrawjs/quickdraw/issues", "publishConfig": { "access": "public" } diff --git a/scripts/sponsors.mjs b/scripts/sponsors.mjs new file mode 100644 index 0000000..05a6db6 --- /dev/null +++ b/scripts/sponsors.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +// Refreshes docs/sponsors.json from the GitHub Sponsors GraphQL API, so the +// website's sponsor wall updates itself — nobody edits a list by hand when a +// sponsorship starts or lapses. Same shape as star-history: CI runs this on a +// schedule and commits the snapshot; the static build just reads the file. +// +// Reading sponsorships needs an authenticated token. The Actions token can +// usually read an org's public sponsors; if GitHub rejects it, add a classic +// PAT with read:org as the SPONSORS_TOKEN repo secret (the workflow prefers +// it when present). +// +// GITHUB_TOKEN= node scripts/sponsors.mjs + +import { writeFileSync, mkdirSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' + +const LOGIN = process.env.SPONSORS_LOGIN ?? 'quickdrawjs' +const OUT = join(dirname(fileURLToPath(import.meta.url)), '..', 'docs') + +const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN +if (!token) { + console.error('sponsors: GITHUB_TOKEN is required — the Sponsors API rejects anonymous reads.') + process.exit(1) +} + +const SPONSOR_FIELDS = ` + totalCount + nodes { + createdAt + isOneTimePayment + tier { monthlyPriceInDollars } + sponsorEntity { + ... on User { login name avatarUrl url websiteUrl } + ... on Organization { login name avatarUrl url websiteUrl } + } + } +` + +// The profile might live on an org or a user account — ask for both and take +// whichever exists. includePrivate stays false: private sponsors asked not to +// be shown, and this file is public. +const QUERY = ` + query($login: String!) { + organization(login: $login) { + sponsorshipsAsMaintainer(first: 100, activeOnly: true, includePrivate: false, orderBy: { field: CREATED_AT, direction: ASC }) { ${SPONSOR_FIELDS} } + } + user(login: $login) { + sponsorshipsAsMaintainer(first: 100, activeOnly: true, includePrivate: false, orderBy: { field: CREATED_AT, direction: ASC }) { ${SPONSOR_FIELDS} } + } + } +` + +const res = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'quickdraw-sponsors', + }, + body: JSON.stringify({ query: QUERY, variables: { login: LOGIN } }), +}) +if (!res.ok) { + console.error(`sponsors: GraphQL request failed — ${res.status} ${res.statusText}`) + process.exit(1) +} +const payload = await res.json() + +// "Could not resolve to an Organization" for the unused half is expected — +// only bail when neither half resolved. +const conn = + payload.data?.organization?.sponsorshipsAsMaintainer ?? + payload.data?.user?.sponsorshipsAsMaintainer +if (!conn) { + console.error('sponsors: no sponsorable account found for', LOGIN, JSON.stringify(payload.errors ?? payload)) + process.exit(1) +} + +const sponsors = (conn.nodes ?? []) + .filter((n) => n?.sponsorEntity?.login) + .map((n) => ({ + login: n.sponsorEntity.login, + name: n.sponsorEntity.name || n.sponsorEntity.login, + url: n.sponsorEntity.websiteUrl || n.sponsorEntity.url, + avatar: n.sponsorEntity.avatarUrl, + monthly: n.tier?.monthlyPriceInDollars ?? 0, + oneTime: !!n.isOneTimePayment, + since: n.createdAt?.slice(0, 10) ?? null, + })) + +mkdirSync(OUT, { recursive: true }) +writeFileSync( + join(OUT, 'sponsors.json'), + JSON.stringify({ generatedAt: new Date().toISOString(), login: LOGIN, sponsors }, null, 2) + '\n', +) +console.log(`sponsors: wrote ${sponsors.length} sponsor(s) to docs/sponsors.json`) diff --git a/scripts/star-history.mjs b/scripts/star-history.mjs index 681df15..935c04e 100644 --- a/scripts/star-history.mjs +++ b/scripts/star-history.mjs @@ -19,7 +19,7 @@ import { dirname, join } from 'node:path' import { buildStarChart } from '../apps/website/src/lib/starChart.js' import { renderStarSvg, THEMES, VIEW } from './lib/starSvg.mjs' -const REPO = process.env.STAR_HISTORY_REPO ?? 'nmndwivedi/quickdraw' +const REPO = process.env.STAR_HISTORY_REPO ?? 'quickdrawjs/quickdraw' const API = `https://api.github.com/repos/${REPO}` const OUT = join(dirname(fileURLToPath(import.meta.url)), '..', 'docs') const PER_PAGE = 100