diff --git a/studio/frontend/src/components/ui/confetti.tsx b/studio/frontend/src/components/ui/confetti.tsx index acd618d218..892bffdb18 100644 --- a/studio/frontend/src/components/ui/confetti.tsx +++ b/studio/frontend/src/components/ui/confetti.tsx @@ -1,150 +1,118 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -import type { - GlobalOptions as ConfettiGlobalOptions, - CreateTypes as ConfettiInstance, - Options as ConfettiOptions, -} from "canvas-confetti"; -import confetti from "canvas-confetti"; -import type { ReactNode } from "react"; -import type React from "react"; -import { - createContext, - forwardRef, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, -} from "react"; - -import { Button } from "@/components/ui/button"; - -type Api = { - fire: (options?: ConfettiOptions) => void; -}; - -type Props = React.ComponentPropsWithRef<"canvas"> & { - options?: ConfettiOptions; - globalOptions?: ConfettiGlobalOptions; - manualstart?: boolean; - children?: ReactNode; -}; - -export type ConfettiRef = Api | null; - -const ConfettiContext = createContext({} as Api); - -// Define component first -const ConfettiComponent = forwardRef((props, ref) => { - const { - options, - globalOptions = { resize: true, useWorker: true }, - manualstart = false, - children, - ...rest - } = props; - const instanceRef = useRef(null); - - const canvasRef = useCallback( - (node: HTMLCanvasElement) => { - if (node !== null) { - if (instanceRef.current) return; - instanceRef.current = confetti.create(node, { - ...globalOptions, - resize: true, - }); - } else { - if (instanceRef.current) { - instanceRef.current.reset(); - instanceRef.current = null; - } - } - }, - [globalOptions], - ); - - const fire = useCallback( - async (opts = {}) => { - try { - await instanceRef.current?.({ ...options, ...opts }); - } catch (error) { - console.error("Confetti error:", error); - } - }, - [options], - ); - - const api = useMemo( - () => ({ - fire, - }), - [fire], - ); - - useImperativeHandle(ref, () => api, [api]); - - useEffect(() => { - if (!manualstart) { - (async () => { - try { - await fire(); - } catch (error) { - console.error("Confetti effect error:", error); - } - })(); - } - }, [manualstart, fire]); - - return ( - - - {children} - - ); -}); - -// Set display name immediately -ConfettiComponent.displayName = "Confetti"; - -// Export as Confetti -export const Confetti = ConfettiComponent; - -interface ConfettiButtonProps extends React.ComponentProps<"button"> { - options?: ConfettiOptions & - ConfettiGlobalOptions & { canvas?: HTMLCanvasElement }; -} - -const ConfettiButtonComponent = ({ - options, - children, - ...props -}: ConfettiButtonProps) => { - const handleClick = async (event: React.MouseEvent) => { - try { - const rect = event.currentTarget.getBoundingClientRect(); - const x = rect.left + rect.width / 2; - const y = rect.top + rect.height / 2; - await confetti({ - ...options, - origin: { - x: x / window.innerWidth, - y: y / window.innerHeight, - }, - }); - } catch (error) { - console.error("Confetti button error:", error); - } - }; - - return ( - - ); -}; - -ConfettiButtonComponent.displayName = "ConfettiButton"; - -export const ConfettiButton = ConfettiButtonComponent; +import type { + GlobalOptions as ConfettiGlobalOptions, + CreateTypes as ConfettiInstance, + Options as ConfettiOptions, +} from "canvas-confetti"; +import confetti from "canvas-confetti"; +import type { ReactNode } from "react"; +import type React from "react"; +import { + createContext, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from "react"; + +type Api = { + fire: (options?: ConfettiOptions) => void; +}; + +type Props = React.ComponentPropsWithRef<"canvas"> & { + options?: ConfettiOptions; + globalOptions?: ConfettiGlobalOptions; + manualstart?: boolean; + children?: ReactNode; +}; + +export type ConfettiRef = Api | null; + +const ConfettiContext = createContext({} as Api); + +// Studio CSP blocks canvas-confetti's default blob: worker, so force +// useWorker: false. Module-scoped so the prop default keeps stable +// identity across renders (`canvasRef` depends on `globalOptions`). +const DEFAULT_GLOBAL_OPTIONS: ConfettiGlobalOptions = { + resize: true, + useWorker: false, +}; + +const ConfettiComponent = forwardRef((props, ref) => { + const { + options, + globalOptions = DEFAULT_GLOBAL_OPTIONS, + manualstart = false, + children, + ...rest + } = props; + const instanceRef = useRef(null); + + const canvasRef = useCallback( + (node: HTMLCanvasElement) => { + if (node !== null) { + if (instanceRef.current) return; + instanceRef.current = confetti.create(node, { + ...globalOptions, + resize: true, + // Force off after the spread so caller globalOptions can't + // re-enable the worker and trip CSP. + useWorker: false, + }); + } else { + if (instanceRef.current) { + instanceRef.current.reset(); + instanceRef.current = null; + } + } + }, + [globalOptions], + ); + + const fire = useCallback( + async (opts = {}) => { + try { + await instanceRef.current?.({ ...options, ...opts }); + } catch (error) { + console.error("Confetti error:", error); + } + }, + [options], + ); + + const api = useMemo( + () => ({ + fire, + }), + [fire], + ); + + useImperativeHandle(ref, () => api, [api]); + + useEffect(() => { + if (!manualstart) { + (async () => { + try { + await fire(); + } catch (error) { + console.error("Confetti effect error:", error); + } + })(); + } + }, [manualstart, fire]); + + return ( + + + {children} + + ); +}); + +ConfettiComponent.displayName = "Confetti"; + +export const Confetti = ConfettiComponent; diff --git a/studio/frontend/src/features/onboarding/components/splash-screen.tsx b/studio/frontend/src/features/onboarding/components/splash-screen.tsx index ce828ee7b4..70438a04d8 100644 --- a/studio/frontend/src/features/onboarding/components/splash-screen.tsx +++ b/studio/frontend/src/features/onboarding/components/splash-screen.tsx @@ -20,7 +20,7 @@ export function SplashScreen({ {/* Mascot */}
= { - 1: "/Sloth emojis/large sloth wave.png", - 2: "/Sloth emojis/sloth magnify final.png", - 3: "/Sloth emojis/sloth huglove large.png", - 4: "/Sloth emojis/large sloth glasses.png", - 5: "/Sloth emojis/large sloth yay.png", + 1: `${import.meta.env.BASE_URL}Sloth emojis/large sloth wave.png`, + 2: `${import.meta.env.BASE_URL}Sloth emojis/sloth magnify final.png`, + 3: `${import.meta.env.BASE_URL}Sloth emojis/sloth huglove large.png`, + 4: `${import.meta.env.BASE_URL}Sloth emojis/large sloth glasses.png`, + 5: `${import.meta.env.BASE_URL}Sloth emojis/large sloth yay.png`, }; export function WizardContent() { diff --git a/studio/frontend/src/features/onboarding/components/wizard-sidebar.tsx b/studio/frontend/src/features/onboarding/components/wizard-sidebar.tsx index d22ac19e51..4637336c6c 100644 --- a/studio/frontend/src/features/onboarding/components/wizard-sidebar.tsx +++ b/studio/frontend/src/features/onboarding/components/wizard-sidebar.tsx @@ -18,7 +18,7 @@ export function WizardSidebar({ returnTo }: { returnTo: string }) {