diff --git a/apps/array/src/renderer/components/CursorGlow.tsx b/apps/array/src/renderer/components/CursorGlow.tsx index 59051f920..5a79065fe 100644 --- a/apps/array/src/renderer/components/CursorGlow.tsx +++ b/apps/array/src/renderer/components/CursorGlow.tsx @@ -1,6 +1,6 @@ import { useSettingsStore } from "@features/settings/stores/settingsStore"; import { useThemeStore } from "@stores/themeStore"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; export function CursorGlow() { const isDarkMode = useThemeStore((state) => state.isDarkMode); @@ -8,6 +8,9 @@ export function CursorGlow() { const [mousePos, setMousePos] = useState<{ x: number; y: number } | null>( null, ); + const [flicker, setFlicker] = useState({ scale: 1, opacity: 0.6 }); + const animationRef = useRef(null); + const lastUpdateRef = useRef(0); useEffect(() => { const handleMouseMove = (e: MouseEvent) => { @@ -18,20 +21,58 @@ export function CursorGlow() { return () => window.removeEventListener("mousemove", handleMouseMove); }, []); + // Torch flicker animation - slow breathing with subtle opacity flicker + useEffect(() => { + if (!isDarkMode || !cursorGlow) return; + + const animate = (timestamp: number) => { + // Slow breathing for size (4 second cycle) + const breathCycle = (timestamp / 4000) * Math.PI * 2; + const breath = Math.sin(breathCycle); + + // Faster subtle opacity flicker (update every ~150ms) + if (timestamp - lastUpdateRef.current > 150) { + lastUpdateRef.current = timestamp; + } + const flickerAmount = 0.75 + Math.random() * 0.25; // 0.75 to 1.0 + + setFlicker({ + scale: 1 + breath * 0.02, + opacity: flickerAmount, + }); + animationRef.current = requestAnimationFrame(animate); + }; + + animationRef.current = requestAnimationFrame(animate); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [isDarkMode, cursorGlow]); + if (!isDarkMode || !cursorGlow || !mousePos) return null; + const baseSize = 200; + const size = baseSize * flicker.scale; + const offset = size / 2; + return (
); diff --git a/apps/array/src/renderer/components/StatusBar.tsx b/apps/array/src/renderer/components/StatusBar.tsx index 4286dd0c8..3037bcfe0 100644 --- a/apps/array/src/renderer/components/StatusBar.tsx +++ b/apps/array/src/renderer/components/StatusBar.tsx @@ -1,5 +1,6 @@ import { CampfireToggle } from "@components/CampfireToggle"; import { StatusBarMenu } from "@components/StatusBarMenu"; +import { TorchToggle } from "@components/TorchToggle"; import { Badge, Box, Code, Flex, Kbd } from "@radix-ui/themes"; import { useStatusBarStore } from "@stores/statusBarStore"; @@ -44,6 +45,7 @@ export function StatusBar({ showKeyHints = true }: StatusBarProps) { )} + {IS_DEV && ( diff --git a/apps/array/src/renderer/components/TorchToggle.tsx b/apps/array/src/renderer/components/TorchToggle.tsx new file mode 100644 index 000000000..d0c2b87a3 --- /dev/null +++ b/apps/array/src/renderer/components/TorchToggle.tsx @@ -0,0 +1,27 @@ +import { useSettingsStore } from "@features/settings/stores/settingsStore"; +import { Fire } from "@phosphor-icons/react"; +import { IconButton, Tooltip } from "@radix-ui/themes"; +import { useThemeStore } from "@stores/themeStore"; + +export function TorchToggle() { + const isDarkMode = useThemeStore((state) => state.isDarkMode); + const { cursorGlow, setCursorGlow } = useSettingsStore(); + + // Only show torch toggle in dark mode + if (!isDarkMode) return null; + + return ( + + setCursorGlow(!cursorGlow)} + style={{ + color: cursorGlow ? "var(--orange-9)" : "var(--gray-9)", + }} + > + + + + ); +}