From 30bf0f6a3c78c4e53db70532de1fd330a4201046 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Mon, 2 Nov 2026 22:58:08 -0800 Subject: [PATCH 1/3] feat(web): celebrate a GitHub star with confetti Co-Authored-By: Claude Opus 5.5 --- apps/web/package.json | 2 ++ apps/web/src/components/GitHubStarPrompt.tsx | 22 +++++++++++++++++++- pnpm-lock.yaml | 16 ++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 3cb58dec9cd6c..1d79dc07cd2ed 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -54,6 +54,7 @@ "@tiptap/pm": "^3.31.3", "@tiptap/react": "^3.31.3", "@tiptap/starter-kit": "^3.31.3", + "canvas-confetti": "1.9.4", "class-variance-authority": "^0.7.1", "culori": "^4.0.2", "dompurify": "^3.4.16", @@ -97,6 +98,7 @@ "@effect/vitest": "catalog:", "@tailwindcss/vite": "^4.0.0", "@tanstack/router-plugin": "^1.161.0", + "@types/canvas-confetti": "1.9.0", "@types/compression": "^1.8.1", "@types/culori": "^4.0.1", "@types/hast": "^3.0.4", diff --git a/apps/web/src/components/GitHubStarPrompt.tsx b/apps/web/src/components/GitHubStarPrompt.tsx index f79714d7a8939..5db8240858560 100644 --- a/apps/web/src/components/GitHubStarPrompt.tsx +++ b/apps/web/src/components/GitHubStarPrompt.tsx @@ -39,7 +39,13 @@ function showStarPrompt(environmentId: EnvironmentId) { toastManager.update(id, { actionProps: { children: "Starring…", disabled: true } }); if (await starRepository(environmentId)) { close(); - toastManager.add({ type: "success", title: "Thanks for the star!", timeout: 4_000 }); + toastManager.add({ + type: "success", + title: "Thanks for the star!", + description: "It really helps T3 Code grow.", + timeout: 5_000, + }); + celebrate(); return; } // Browsers block new tabs once the click's activation expires; desktop opens through its bridge. @@ -62,6 +68,20 @@ function showStarPrompt(environmentId: EnvironmentId) { }); } +// One burst from the toast corner. The library loads only here, and its canvas stops when done. +function celebrate() { + void import("canvas-confetti") + .then(({ default: confetti }) => + confetti({ + particleCount: 120, + spread: 70, + origin: { x: 0.85, y: 0.15 }, + disableForReducedMotion: true, + }), + ) + .catch(() => undefined); +} + // Resolves after IDLE_MS without input while this window has focus. function waitForIdle() { return new Promise((resolve) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84a19021c78c8..755f2f95c2ed1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1143,6 +1143,9 @@ importers: '@tiptap/starter-kit': specifier: ^3.31.3 version: 3.31.3 + canvas-confetti: + specifier: 1.9.4 + version: 1.9.4 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -1267,6 +1270,9 @@ importers: '@tanstack/router-plugin': specifier: ^1.161.0 version: 1.168.13(@tanstack/react-router@1.170.10(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@voidzero-dev/vite-plus-core@1.4.0(@tsc-rs/typescript@0.8.0)(@types/node@24.12.4)(@volar/typescript@2.4.28(@tsc-rs/typescript@0.8.0))(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(unrun@0.2.39)(yaml@2.9.0))(supports-color@8.1.1) + '@types/canvas-confetti': + specifier: 1.9.0 + version: 1.9.0 '@types/compression': specifier: ^1.8.1 version: 1.8.1 @@ -7008,6 +7014,9 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/canvas-confetti@1.9.0': + resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -7947,6 +7956,9 @@ packages: caniuse-lite@1.0.30001810: resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + canvas-confetti@1.9.4: + resolution: {integrity: sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==} + capnp-es@0.0.16: resolution: {integrity: sha512-pqhhnRqGfDdgHa+rz4JVO9j1jnkyqsHRYrD4RBtIwxCknYSRG7Mjs+x3IRfd20u+ZGS0IhW2L7cDv9ZcIkCYhQ==} hasBin: true @@ -16574,6 +16586,8 @@ snapshots: '@types/node': 24.12.4 '@types/responselike': 1.0.3 + '@types/canvas-confetti@1.9.0': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -17735,6 +17749,8 @@ snapshots: caniuse-lite@1.0.30001810: {} + canvas-confetti@1.9.4: {} + capnp-es@0.0.16(@tsc-rs/typescript@0.8.0): optionalDependencies: typescript: '@tsc-rs/typescript@0.8.0' From 914c659d8b732ce74f0e1c2a92e237c14f78d9f7 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Mon, 2 Nov 2026 23:05:44 -0800 Subject: [PATCH 2/3] feat(web): make the star confetti burst explosive Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/GitHubStarPrompt.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/GitHubStarPrompt.tsx b/apps/web/src/components/GitHubStarPrompt.tsx index 5db8240858560..701334b7d2926 100644 --- a/apps/web/src/components/GitHubStarPrompt.tsx +++ b/apps/web/src/components/GitHubStarPrompt.tsx @@ -68,14 +68,17 @@ function showStarPrompt(environmentId: EnvironmentId) { }); } -// One burst from the toast corner. The library loads only here, and its canvas stops when done. +// One burst out of the toast. The library loads only here, and its canvas stops when done. function celebrate() { void import("canvas-confetti") .then(({ default: confetti }) => confetti({ - particleCount: 120, - spread: 70, - origin: { x: 0.85, y: 0.15 }, + particleCount: 160, + spread: 360, + startVelocity: 38, + gravity: 1.6, + ticks: 140, + origin: { x: 0.83, y: 0.15 }, disableForReducedMotion: true, }), ) From e10344cae2a46192a2563748c9dc8c6e14950ea2 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Mon, 2 Nov 2026 23:08:58 -0800 Subject: [PATCH 3/3] feat(web): thank stargazers in place so the confetti has no toast slide Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/GitHubStarPrompt.test.ts | 5 +++-- apps/web/src/components/GitHubStarPrompt.tsx | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/GitHubStarPrompt.test.ts b/apps/web/src/components/GitHubStarPrompt.test.ts index f45229d5ce85e..f748ec8b82209 100644 --- a/apps/web/src/components/GitHubStarPrompt.test.ts +++ b/apps/web/src/components/GitHubStarPrompt.test.ts @@ -165,11 +165,12 @@ describe("recordPromptSent", () => { vi.unstubAllGlobals(); }); it("stars on click, opens GitHub when starring fails, and asks for a click once the click expired", async () => { - setup({ sent: 5 }); + const store = setup({ sent: 5 }); await (await startSession()).send(); click(add.mock.lastCall?.[0].actionProps); await vi.advanceTimersByTimeAsync(0); - expect(add.mock.lastCall?.[0].title).toBe("Thanks for the star!"); + expect(update.mock.lastCall?.[1]).toMatchObject({ title: "Thanks for the star!" }); + expect(store.get("t3code:github-star-prompt:dismissed")).toBe("true"); expect(openExternal).not.toHaveBeenCalled(); setup({ sent: 5 }); diff --git a/apps/web/src/components/GitHubStarPrompt.tsx b/apps/web/src/components/GitHubStarPrompt.tsx index 701334b7d2926..679d61b1f3502 100644 --- a/apps/web/src/components/GitHubStarPrompt.tsx +++ b/apps/web/src/components/GitHubStarPrompt.tsx @@ -38,12 +38,15 @@ function showStarPrompt(environmentId: EnvironmentId) { const starInPlace = async () => { toastManager.update(id, { actionProps: { children: "Starring…", disabled: true } }); if (await starRepository(environmentId)) { - close(); - toastManager.add({ + // Thank them in place: a closing and opening toast would slide under the confetti. + dismiss(); + toastManager.update(id, { type: "success", title: "Thanks for the star!", description: "It really helps T3 Code grow.", timeout: 5_000, + actionProps: undefined, + data: {}, }); celebrate(); return;