Merge branch 'MODSetter:dev' into dev

This commit is contained in:
Eric Lammertsma 2026-02-05 11:57:31 -05:00 committed by GitHub
commit aefe1b6e0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 834 additions and 63 deletions

View file

@ -0,0 +1,33 @@
"""Add DISCORD_JOIN to incentive task type enum
Revision ID: 91
Revises: 90
Changes:
1. Add DISCORD_JOIN value to incentivetasktype enum
"""
from collections.abc import Sequence
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "91"
down_revision: str | None = "90"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Add DISCORD_JOIN to incentivetasktype enum."""
op.execute("ALTER TYPE incentivetasktype ADD VALUE IF NOT EXISTS 'DISCORD_JOIN'")
def downgrade() -> None:
"""Remove DISCORD_JOIN from incentivetasktype enum.
Note: PostgreSQL doesn't support removing values from enums directly.
This would require recreating the enum type, which is complex and risky.
For safety, we leave the enum value in place during downgrade.
"""
pass

View file

@ -164,6 +164,7 @@ class IncentiveTaskType(str, Enum):
GITHUB_STAR = "GITHUB_STAR" GITHUB_STAR = "GITHUB_STAR"
REDDIT_FOLLOW = "REDDIT_FOLLOW" REDDIT_FOLLOW = "REDDIT_FOLLOW"
DISCORD_JOIN = "DISCORD_JOIN"
# Future tasks can be added here: # Future tasks can be added here:
# GITHUB_ISSUE = "GITHUB_ISSUE" # GITHUB_ISSUE = "GITHUB_ISSUE"
# SOCIAL_SHARE = "SOCIAL_SHARE" # SOCIAL_SHARE = "SOCIAL_SHARE"
@ -185,6 +186,12 @@ INCENTIVE_TASKS_CONFIG = {
"pages_reward": 100, "pages_reward": 100,
"action_url": "https://www.reddit.com/r/SurfSense/", "action_url": "https://www.reddit.com/r/SurfSense/",
}, },
IncentiveTaskType.DISCORD_JOIN: {
"title": "Join our Discord",
"description": "Join the SurfSense community on Discord",
"pages_reward": 100,
"action_url": "https://discord.gg/ejRNvftDp9",
},
# Future tasks can be configured here: # Future tasks can be configured here:
# IncentiveTaskType.GITHUB_ISSUE: { # IncentiveTaskType.GITHUB_ISSUE: {
# "title": "Create an issue", # "title": "Create an issue",

View file

@ -1,71 +1,802 @@
"use client"; "use client";
import { MoonIcon, SunIcon } from "lucide-react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useId, useState } from "react";
import { Button } from "@/components/ui/button";
export function ThemeTogglerComponent() { import { cn } from "@/lib/utils";
const { theme, setTheme } = useTheme();
const [isClient, setIsClient] = useState(false); // ///////////////////////////////////////////////////////////////////////////
// Types
export type AnimationVariant =
| "circle"
| "rectangle"
| "gif"
| "polygon"
| "circle-blur";
export type AnimationStart =
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right"
| "center"
| "top-center"
| "bottom-center"
| "bottom-up"
| "top-down"
| "left-right"
| "right-left";
useEffect(() => { interface Animation {
setIsClient(true); name: string;
}, []); css: string;
return (
isClient && (
<Button
variant="ghost"
onClick={() => {
theme === "dark" ? setTheme("light") : setTheme("dark");
}}
className="w-8 h-8 flex hover:bg-gray-50 dark:hover:bg-white/[0.1] rounded-lg items-center cursor-pointer justify-center outline-none focus:ring-0 focus:outline-none active:ring-0 active:outline-none overflow-hidden"
>
{theme === "light" && (
<motion.div
key={theme}
initial={{
x: 40,
opacity: 0,
}}
animate={{
x: 0,
opacity: 1,
}}
transition={{
duration: 0.3,
ease: "easeOut",
}}
>
<SunIcon className="h-4 w-4 flex-shrink-0 dark:text-neutral-500 text-neutral-700" />
</motion.div>
)}
{theme === "dark" && (
<motion.div
key={theme}
initial={{
x: 40,
opacity: 0,
}}
animate={{
x: 0,
opacity: 1,
}}
transition={{
ease: "easeOut",
duration: 0.3,
}}
>
<MoonIcon className="h-4 w-4 flex-shrink-0 " />
</motion.div>
)}
<span className="sr-only">Toggle theme</span>
</Button>
)
);
} }
// ///////////////////////////////////////////////////////////////////////////
// Helper functions
const getPositionCoords = (position: AnimationStart) => {
switch (position) {
case "top-left":
return { cx: "0", cy: "0" };
case "top-right":
return { cx: "40", cy: "0" };
case "bottom-left":
return { cx: "0", cy: "40" };
case "bottom-right":
return { cx: "40", cy: "40" };
case "top-center":
return { cx: "20", cy: "0" };
case "bottom-center":
return { cx: "20", cy: "40" };
case "bottom-up":
case "top-down":
case "left-right":
case "right-left":
return { cx: "20", cy: "20" };
}
};
const generateSVG = (variant: AnimationVariant, start: AnimationStart) => {
if (variant === "circle-blur") {
if (start === "center") {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="20" cy="20" r="18" fill="white" filter="url(%23blur)"/></svg>`;
}
const positionCoords = getPositionCoords(start);
if (!positionCoords) {
throw new Error(`Invalid start position: ${start}`);
}
const { cx, cy } = positionCoords;
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="${cx}" cy="${cy}" r="18" fill="white" filter="url(%23blur)"/></svg>`;
}
if (start === "center") return;
if (variant === "rectangle") return "";
const positionCoords = getPositionCoords(start);
if (!positionCoords) {
throw new Error(`Invalid start position: ${start}`);
}
const { cx, cy } = positionCoords;
if (variant === "circle") {
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><circle cx="${cx}" cy="${cy}" r="20" fill="white"/></svg>`;
}
return "";
};
const getTransformOrigin = (start: AnimationStart) => {
switch (start) {
case "top-left":
return "top left";
case "top-right":
return "top right";
case "bottom-left":
return "bottom left";
case "bottom-right":
return "bottom right";
case "top-center":
return "top center";
case "bottom-center":
return "bottom center";
case "bottom-up":
case "top-down":
case "left-right":
case "right-left":
return "center";
}
};
export const createAnimation = (
variant: AnimationVariant,
start: AnimationStart = "center",
blur = false,
url?: string,
): Animation => {
const svg = generateSVG(variant, start);
const transformOrigin = getTransformOrigin(start);
if (variant === "rectangle") {
const getClipPath = (direction: AnimationStart) => {
switch (direction) {
case "bottom-up":
return {
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-down":
return {
from: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "left-right":
return {
from: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "right-left":
return {
from: "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-left":
return {
from: "polygon(0% 0%, 0% 0%, 0% 0%, 0% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "top-right":
return {
from: "polygon(100% 0%, 100% 0%, 100% 0%, 100% 0%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "bottom-left":
return {
from: "polygon(0% 100%, 0% 100%, 0% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
case "bottom-right":
return {
from: "polygon(100% 100%, 100% 100%, 100% 100%, 100% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
default:
return {
from: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",
to: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
};
}
};
const clipPath = getClipPath(start);
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 0.7s;
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
animation-name: reveal-light-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
z-index: -1;
}
.dark::view-transition-new(root) {
animation-name: reveal-dark-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
@keyframes reveal-dark-${start}${blur ? "-blur" : ""} {
from {
clip-path: ${clipPath.from};
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: ${clipPath.to};
${blur ? "filter: blur(0px);" : ""}
}
}
@keyframes reveal-light-${start}${blur ? "-blur" : ""} {
from {
clip-path: ${clipPath.from};
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: ${clipPath.to};
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
}
if (variant === "circle" && start == "center") {
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 0.7s;
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
animation-name: reveal-light${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
z-index: -1;
}
.dark::view-transition-new(root) {
animation-name: reveal-dark${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
@keyframes reveal-dark${blur ? "-blur" : ""} {
from {
clip-path: circle(0% at 50% 50%);
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: circle(100.0% at 50% 50%);
${blur ? "filter: blur(0px);" : ""}
}
}
@keyframes reveal-light${blur ? "-blur" : ""} {
from {
clip-path: circle(0% at 50% 50%);
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: circle(100.0% at 50% 50%);
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
}
if (variant === "gif") {
return {
name: `${variant}-${start}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-in);
}
::view-transition-new(root) {
mask: url('${url}') center / 0 no-repeat;
animation: scale 3s;
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: scale 3s;
}
@keyframes scale {
0% {
mask-size: 0;
}
10% {
mask-size: 50vmax;
}
90% {
mask-size: 50vmax;
}
100% {
mask-size: 2000vmax;
}
}`,
};
}
if (variant === "circle-blur") {
if (start === "center") {
return {
name: `${variant}-${start}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
mask: url('${svg}') center / 0 no-repeat;
mask-origin: content-box;
animation: scale 1s;
transform-origin: center;
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: scale 1s;
transform-origin: center;
z-index: -1;
}
@keyframes scale {
to {
mask-size: 350vmax;
}
}
`,
};
}
return {
name: `${variant}-${start}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
mask: url('${svg}') ${start.replace("-", " ")} / 0 no-repeat;
mask-origin: content-box;
animation: scale 1s;
transform-origin: ${transformOrigin};
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: scale 1s;
transform-origin: ${transformOrigin};
z-index: -1;
}
@keyframes scale {
to {
mask-size: 350vmax;
}
}
`,
};
}
if (variant === "polygon") {
const getPolygonClipPaths = (position: AnimationStart) => {
switch (position) {
case "top-left":
return {
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
};
case "top-right":
return {
darkFrom: "polygon(150% -71%, 250% 71%, 250% 71%, 150% -71%)",
darkTo: "polygon(150% -71%, 250% 71%, 50% 171%, -71% 50%)",
lightFrom: "polygon(-71% 50%, 50% 171%, 50% 171%, -71% 50%)",
lightTo: "polygon(-71% 50%, 50% 171%, 250% 71%, 150% -71%)",
};
default:
return {
darkFrom: "polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%)",
darkTo: "polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%)",
lightFrom: "polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%)",
lightTo: "polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%)",
};
}
};
const clipPaths = getPolygonClipPaths(start);
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 0.7s;
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
animation-name: reveal-light-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
z-index: -1;
}
.dark::view-transition-new(root) {
animation-name: reveal-dark-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
@keyframes reveal-dark-${start}${blur ? "-blur" : ""} {
from {
clip-path: ${clipPaths.darkFrom};
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: ${clipPaths.darkTo};
${blur ? "filter: blur(0px);" : ""}
}
}
@keyframes reveal-light-${start}${blur ? "-blur" : ""} {
from {
clip-path: ${clipPaths.lightFrom};
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: ${clipPaths.lightTo};
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
}
// Handle circle variants with start positions using clip-path
if (variant === "circle" && start !== "center") {
const getClipPathPosition = (position: AnimationStart) => {
switch (position) {
case "top-left":
return "0% 0%";
case "top-right":
return "100% 0%";
case "bottom-left":
return "0% 100%";
case "bottom-right":
return "100% 100%";
case "top-center":
return "50% 0%";
case "bottom-center":
return "50% 100%";
default:
return "50% 50%";
}
};
const clipPosition = getClipPathPosition(start);
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-duration: 1s;
animation-timing-function: var(--expo-out);
}
::view-transition-new(root) {
animation-name: reveal-light-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
z-index: -1;
}
.dark::view-transition-new(root) {
animation-name: reveal-dark-${start}${blur ? "-blur" : ""};
${blur ? "filter: blur(2px);" : ""}
}
@keyframes reveal-dark-${start}${blur ? "-blur" : ""} {
from {
clip-path: circle(0% at ${clipPosition});
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: circle(150.0% at ${clipPosition});
${blur ? "filter: blur(0px);" : ""}
}
}
@keyframes reveal-light-${start}${blur ? "-blur" : ""} {
from {
clip-path: circle(0% at ${clipPosition});
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
clip-path: circle(150.0% at ${clipPosition});
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
}
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `
::view-transition-group(root) {
animation-timing-function: var(--expo-in);
}
::view-transition-new(root) {
mask: url('${svg}') ${start.replace("-", " ")} / 0 no-repeat;
mask-origin: content-box;
animation: scale-${start}${blur ? "-blur" : ""} 1s;
transform-origin: ${transformOrigin};
${blur ? "filter: blur(2px);" : ""}
}
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: scale-${start}${blur ? "-blur" : ""} 1s;
transform-origin: ${transformOrigin};
z-index: -1;
}
@keyframes scale-${start}${blur ? "-blur" : ""} {
from {
${blur ? "filter: blur(8px);" : ""}
}
${blur ? "50% { filter: blur(4px); }" : ""}
to {
mask-size: 2000vmax;
${blur ? "filter: blur(0px);" : ""}
}
}
`,
};
};
// ///////////////////////////////////////////////////////////////////////////
// Custom hook for theme toggle functionality
export const useThemeToggle = ({
variant = "circle",
start = "center",
blur = false,
gifUrl = "",
}: {
variant?: AnimationVariant;
start?: AnimationStart;
blur?: boolean;
gifUrl?: string;
} = {}) => {
const { theme, setTheme, resolvedTheme } = useTheme();
const [isDark, setIsDark] = useState(false);
// Sync isDark state with resolved theme after hydration
useEffect(() => {
setIsDark(resolvedTheme === "dark");
}, [resolvedTheme]);
const styleId = "theme-transition-styles";
const updateStyles = useCallback((css: string) => {
if (typeof window === "undefined") return;
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
styleElement.textContent = css;
}, []);
const toggleTheme = useCallback(() => {
setIsDark(!isDark);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css);
if (typeof window === "undefined") return;
const switchTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [theme, setTheme, variant, start, blur, gifUrl, updateStyles, isDark]);
const setCrazyLightTheme = useCallback(() => {
setIsDark(false);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css);
if (typeof window === "undefined") return;
const switchTheme = () => {
setTheme("light");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
const setCrazyDarkTheme = useCallback(() => {
setIsDark(true);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css);
if (typeof window === "undefined") return;
const switchTheme = () => {
setTheme("dark");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
const setCrazySystemTheme = useCallback(() => {
if (typeof window === "undefined") return;
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
setIsDark(prefersDark);
const animation = createAnimation(variant, start, blur, gifUrl);
updateStyles(animation.css);
const switchTheme = () => {
setTheme("system");
};
if (!document.startViewTransition) {
switchTheme();
return;
}
document.startViewTransition(switchTheme);
}, [setTheme, variant, start, blur, gifUrl, updateStyles]);
return {
isDark,
setIsDark,
toggleTheme,
setCrazyLightTheme,
setCrazyDarkTheme,
setCrazySystemTheme,
};
};
// ///////////////////////////////////////////////////////////////////////////
// Theme Toggle Button Component (Sun/Moon Style)
export const ThemeToggleButton = ({
className = "",
variant = "circle",
start = "center",
blur = false,
gifUrl = "",
}: {
className?: string;
variant?: AnimationVariant;
start?: AnimationStart;
blur?: boolean;
gifUrl?: string;
}) => {
const { isDark, toggleTheme } = useThemeToggle({
variant,
start,
blur,
gifUrl,
});
const clipId = useId();
const clipPathId = `theme-toggle-clip-${clipId}`;
return (
<button
type="button"
className={cn(
"size-10 cursor-pointer rounded-full p-2 transition-all duration-300 active:scale-95 bg-transparent",
isDark ? "text-white" : "text-black",
className,
)}
onClick={toggleTheme}
aria-label="Toggle theme"
>
<span className="sr-only">Toggle theme</span>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="currentColor"
strokeLinecap="round"
viewBox="0 0 32 32"
>
<clipPath id={clipPathId}>
<motion.path
animate={{ y: isDark ? 10 : 0, x: isDark ? -12 : 0 }}
transition={{ ease: "easeInOut", duration: 0.35 }}
d="M0-5h30a1 1 0 0 0 9 13v24H0Z"
/>
</clipPath>
<g clipPath={`url(#${clipPathId})`}>
<motion.circle
animate={{ r: isDark ? 10 : 8 }}
transition={{ ease: "easeInOut", duration: 0.35 }}
cx="16"
cy="16"
/>
<motion.g
animate={{
rotate: isDark ? -100 : 0,
scale: isDark ? 0.5 : 1,
opacity: isDark ? 0 : 1,
}}
transition={{ ease: "easeInOut", duration: 0.35 }}
stroke="currentColor"
strokeWidth="1.5"
>
<path d="M16 5.5v-4" />
<path d="M16 30.5v-4" />
<path d="M1.5 16h4" />
<path d="M26.5 16h4" />
<path d="m23.4 8.6 2.8-2.8" />
<path d="m5.7 26.3 2.9-2.9" />
<path d="m5.8 5.8 2.8 2.8" />
<path d="m23.4 23.4 2.9 2.9" />
</motion.g>
</g>
</svg>
</button>
);
};
// ///////////////////////////////////////////////////////////////////////////
// Backwards compatible export (alias for ThemeToggleButton with default settings)
export function ThemeTogglerComponent() {
return (
<ThemeToggleButton
variant="circle"
start="top-right"
className="size-8"
/>
);
}
/**
* Skiper 26 Theme_buttons_002 React + CSS + transition view api https://developer.chrome.com/docs/web-platform/view-transitions/
* Original concept from rudrodip
* Inspired by from https://github.com/rudrodip/theme-toggle-effect
* We respect the original creators. This is an inspired rebuild with our own taste and does not claim any ownership.
* These animations aren't associated with the rudrodip . They're independent recreations meant to study interaction design
*
* License & Usage:
* - Free to use and modify in both personal and commercial projects.
* - Attribution to Skiper UI is required when using the free version.
* - No attribution required with Skiper UI Pro.
*
* Feedback and contributions are welcome.
*
* Author: @gurvinder-singh02
* Website: https://gxuri.in
* Twitter: https://x.com/Gur__vi
*/