diff --git a/src/components/admin/ThemeTab.tsx b/src/components/admin/ThemeTab.tsx index 3bfad07..1eaaef2 100644 --- a/src/components/admin/ThemeTab.tsx +++ b/src/components/admin/ThemeTab.tsx @@ -149,24 +149,47 @@ export function ThemeTab() { }, }); - // Update a single color in the draft and apply preview instantly + // Throttle applyThemeColors to once per animation frame + const rafRef = useRef(0); + const querySyncRef = useRef(0); + const updateDraftColor = useCallback( (key: keyof ThemeColors, value: string) => { setDraftColors((prev) => { const next = { ...prev, [key]: value }; - applyThemeColors(next); - queryClient.setQueryData(['theme-colors'], next); + + // Throttle CSS variable updates to 1x per frame + cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(() => { + applyThemeColors(next); + }); + + // Debounce query cache update (triggers re-renders of other components) + clearTimeout(querySyncRef.current); + querySyncRef.current = window.setTimeout(() => { + queryClient.setQueryData(['theme-colors'], next); + }, 150); + return next; }); }, [queryClient], ); + // Cleanup on unmount + useEffect(() => { + return () => { + cancelAnimationFrame(rafRef.current); + clearTimeout(querySyncRef.current); + }; + }, []); + // Apply a full preset and auto-save to server const applyPreset = useCallback( (colors: Partial) => { setDraftColors((prev) => { const next = { ...prev, ...colors }; + // Preset is a one-shot action — apply immediately (no throttle needed) applyThemeColors(next); queryClient.setQueryData(['theme-colors'], next); // Auto-save preset to server so it persists across navigation diff --git a/src/components/ui/backgrounds/background-beams.tsx b/src/components/ui/backgrounds/background-beams.tsx index e36b828..26a6577 100644 --- a/src/components/ui/backgrounds/background-beams.tsx +++ b/src/components/ui/backgrounds/background-beams.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { motion } from 'framer-motion'; interface Props { settings: Record; } +// All 50 paths from the original Aceternity component const paths = [ 'M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875', 'M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867', @@ -26,13 +26,78 @@ const paths = [ 'M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739', 'M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731', 'M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723', + 'M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715', + 'M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707', + 'M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699', + 'M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691', + 'M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683', + 'M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675', + 'M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667', + 'M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659', + 'M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651', + 'M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643', + 'M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635', + 'M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627', + 'M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619', + 'M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611', + 'M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603', + 'M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595', + 'M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587', + 'M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579', + 'M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571', + 'M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563', + 'M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555', + 'M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547', + 'M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539', + 'M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531', + 'M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523', + 'M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515', + 'M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507', + 'M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499', + 'M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491', + 'M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483', ]; -// Static background paths (subset for the radial gradient overlay) -const bgPath = - 'M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803'; +// Concatenated static background path +const bgPath = paths.join(''); + +// Seeded pseudo-random for deterministic values (avoids hydration mismatches) +function seededRandom(seed: number) { + const x = Math.sin(seed * 9301 + 49297) * 49297; + return x - Math.floor(x); +} + +// Pre-compute beam animation params at module level (stable across renders) +const beamParams = paths.map((_, i) => ({ + duration: 10 + seededRandom(i) * 10, // 10-20s + delay: seededRandom(i + 100) * 10, // 0-10s +})); + +// CSS keyframes injected once +const STYLE_ID = 'bg-beams-style'; +function ensureStyles() { + if (typeof document === 'undefined') return; + if (document.getElementById(STYLE_ID)) return; + + const style = document.createElement('style'); + style.id = STYLE_ID; + style.textContent = ` + @keyframes beamDash { + 0% { stroke-dashoffset: 1; opacity: 0; } + 2% { opacity: 1; } + 80% { opacity: 1; } + 100% { stroke-dashoffset: -1; opacity: 0; } + } + `; + document.head.appendChild(style); +} export default React.memo(function BackgroundBeams({ settings: _settings }: Props) { + // Inject CSS once on first render + React.useEffect(() => { + ensureStyles(); + }, []); + return (
- {/* Static background paths */} - + {/* Static background — all paths at very low opacity */} + - {/* Animated beam paths with moving gradients */} - {paths.map((path, index) => ( - ( + ))} - {/* Animated linear gradients that move along the paths */} - {paths.map((_, index) => ( - - - - - - - ))} + {/* Single shared gradient for all beams (cyan → purple → magenta) */} + + + + + + {/* Radial gradient for static background */}