From 0a39ce21ea5cb5c531f838685a297b6dcd0466e3 Mon Sep 17 00:00:00 2001 From: Masaki Date: Thu, 26 Feb 2026 10:41:51 +0900 Subject: [PATCH 1/4] fix(live-news): call destroyPlayer() before offline/embed error message (fixes #347) Made-with: Cursor --- src/components/LiveNewsPanel.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/LiveNewsPanel.ts b/src/components/LiveNewsPanel.ts index f17e87f62..bad79778a 100644 --- a/src/components/LiveNewsPanel.ts +++ b/src/components/LiveNewsPanel.ts @@ -733,6 +733,7 @@ export class LiveNewsPanel extends Panel { } private showOfflineMessage(channel: LiveChannel): void { + this.destroyPlayer(); this.content.innerHTML = `
📺
@@ -743,6 +744,7 @@ export class LiveNewsPanel extends Panel { } private showEmbedError(channel: LiveChannel, errorCode: number): void { + this.destroyPlayer(); const watchUrl = channel.videoId ? `https://www.youtube.com/watch?v=${encodeURIComponent(channel.videoId)}` : `https://www.youtube.com/${channel.handle}`; From f8c05448a019a88730bead61f688b041eab54c86 Mon Sep 17 00:00:00 2001 From: Masaki Date: Thu, 26 Feb 2026 10:55:38 +0900 Subject: [PATCH 2/4] fix: resolve canvas-confetti import error (CDN load + optimizeDeps) Made-with: Cursor --- src/services/celebration.ts | 53 ++++++++++++++++++++----------------- vite.config.ts | 3 +++ 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/services/celebration.ts b/src/services/celebration.ts index f72fd390f..09600b6e2 100644 --- a/src/services/celebration.ts +++ b/src/services/celebration.ts @@ -9,12 +9,15 @@ * so celebrations feel special, not repetitive. * * Respects prefers-reduced-motion: no animations when that media query matches. + * + * Loads confetti at runtime via CDN script so the app builds and runs without + * requiring the canvas-confetti package in node_modules (no Vite resolve error). */ -import confetti from 'canvas-confetti'; - // ---- Types ---- +type ConfettiFn = (opts: { particleCount: number; spread: number; origin: { y: number }; colors: string[]; disableForReducedMotion: boolean }) => void; + export interface MilestoneData { speciesRecoveries?: Array<{ name: string; status: string }>; renewablePercent?: number; @@ -34,6 +37,21 @@ const WARM_COLORS = ['#6B8F5E', '#C4A35A', '#7BA5C4', '#8BAF7A', '#E8B96E', '#7F /** Session-level dedup set. Stores milestone keys that have already been celebrated this session. */ const celebrated = new Set(); +/** Load confetti from CDN at runtime so Vite never has to resolve the package. */ +function loadConfetti(): Promise { + if (typeof window === 'undefined') return Promise.resolve(null); + const w = window as Window & { confetti?: ConfettiFn }; + if (typeof w.confetti === 'function') return Promise.resolve(w.confetti); + return new Promise((resolve) => { + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/dist/canvas-confetti.min.js'; + script.async = true; + script.onload = () => (typeof w.confetti === 'function' ? resolve(w.confetti!) : resolve(null)); + script.onerror = () => resolve(null); + document.head.appendChild(script); + }); +} + // ---- Public API ---- /** @@ -45,31 +63,18 @@ const celebrated = new Set(); export function celebrate(type: 'milestone' | 'record' = 'milestone'): void { if (REDUCED_MOTION) return; - if (type === 'milestone') { - void confetti({ - particleCount: 40, - spread: 60, - origin: { y: 0.7 }, - colors: WARM_COLORS, - disableForReducedMotion: true, + const run = (opts: { particleCount: number; spread: number; origin: { y: number }; colors: string[] }) => { + void loadConfetti().then((confetti) => { + if (confetti) confetti({ ...opts, disableForReducedMotion: true }); }); + }; + + if (type === 'milestone') { + run({ particleCount: 40, spread: 60, origin: { y: 0.7 }, colors: WARM_COLORS }); } else { - // 'record' -- double burst for extra emphasis - void confetti({ - particleCount: 80, - spread: 90, - origin: { y: 0.6 }, - colors: WARM_COLORS, - disableForReducedMotion: true, - }); + run({ particleCount: 80, spread: 90, origin: { y: 0.6 }, colors: WARM_COLORS }); setTimeout(() => { - void confetti({ - particleCount: 80, - spread: 90, - origin: { y: 0.6 }, - colors: WARM_COLORS, - disableForReducedMotion: true, - }); + run({ particleCount: 80, spread: 90, origin: { y: 0.6 }, colors: WARM_COLORS }); }, 300); } } diff --git a/vite.config.ts b/vite.config.ts index 85e539b64..505bbd164 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -654,6 +654,9 @@ export default defineConfig({ define: { __APP_VERSION__: JSON.stringify(pkg.version), }, + optimizeDeps: { + include: ['canvas-confetti'], + }, plugins: [ htmlVariantPlugin(), polymarketPlugin(), From 7b481b3aee21aa0d832263ffeebbc084ca41af92 Mon Sep 17 00:00:00 2001 From: Masaki Date: Thu, 26 Feb 2026 19:36:50 +0900 Subject: [PATCH 3/4] Revert Fix 2 (canvas-confetti); keep only Fix 1 (Issue #347) Made-with: Cursor --- docs/FIX2_CANVAS_CONFETTI_REWORK.md | 25 ++++++++++++++++ docs/PR_347_BODY.md | 46 +++++++++++++++++++++++++++++ src/services/celebration.ts | 26 ++-------------- vite.config.ts | 3 -- 4 files changed, 74 insertions(+), 26 deletions(-) create mode 100644 docs/FIX2_CANVAS_CONFETTI_REWORK.md create mode 100644 docs/PR_347_BODY.md diff --git a/docs/FIX2_CANVAS_CONFETTI_REWORK.md b/docs/FIX2_CANVAS_CONFETTI_REWORK.md new file mode 100644 index 000000000..f425e184d --- /dev/null +++ b/docs/FIX2_CANVAS_CONFETTI_REWORK.md @@ -0,0 +1,25 @@ +# Fix 2: canvas-confetti — Rework for separate PR + +Review feedback (summary): Fix 2 was split from the Issue #347 PR. When submitting a new PR for the canvas-confetti change, address the following. + +## 1. Promise cache for `loadConfetti()` + +- **Issue:** `loadConfetti()` appends a new `