mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
feat: layout design with theme improvements new improvements on light mode layout chore: settings page with graph header and memories list
229 lines
7.7 KiB
TypeScript
229 lines
7.7 KiB
TypeScript
"use client";
|
|
|
|
import { motion, useReducedMotion } from "motion/react";
|
|
import { useEffect, useMemo, useState, memo } from "react";
|
|
import { useOnboarding } from "./onboarding-context";
|
|
|
|
interface OrbProps {
|
|
size: number;
|
|
initialX: number;
|
|
initialY: number;
|
|
duration: number;
|
|
delay: number;
|
|
revealDelay: number;
|
|
shouldReveal: boolean;
|
|
color: {
|
|
primary: string;
|
|
secondary: string;
|
|
tertiary: string;
|
|
};
|
|
}
|
|
|
|
function FloatingOrb({ size, initialX, initialY, duration, delay, revealDelay, shouldReveal, color }: OrbProps) {
|
|
const blurPixels = Math.min(64, Math.max(24, Math.floor(size * 0.08)));
|
|
|
|
const gradient = useMemo(() => {
|
|
return `radial-gradient(circle, ${color.primary} 0%, ${color.secondary} 40%, ${color.tertiary} 70%, transparent 100%)`;
|
|
}, [color.primary, color.secondary, color.tertiary]);
|
|
|
|
const style = useMemo(() => {
|
|
return {
|
|
width: size,
|
|
height: size,
|
|
background: gradient,
|
|
filter: `blur(${blurPixels}px)`,
|
|
willChange: "transform, opacity",
|
|
mixBlendMode: "plus-lighter",
|
|
} as any;
|
|
}, [size, gradient, blurPixels]);
|
|
|
|
const initial = useMemo(() => {
|
|
return {
|
|
x: initialX,
|
|
y: initialY,
|
|
scale: 0,
|
|
opacity: 0,
|
|
};
|
|
}, [initialX, initialY]);
|
|
|
|
const animate = useMemo(() => {
|
|
if (!shouldReveal) {
|
|
return {
|
|
x: initialX,
|
|
y: initialY,
|
|
scale: 0,
|
|
opacity: 0,
|
|
};
|
|
}
|
|
return {
|
|
x: [initialX, initialX + 200, initialX - 150, initialX + 100, initialX],
|
|
y: [initialY, initialY - 180, initialY + 120, initialY - 80, initialY],
|
|
scale: [0.8, 1.2, 0.9, 1.1, 0.8],
|
|
opacity: 0.7,
|
|
};
|
|
}, [shouldReveal, initialX, initialY]);
|
|
|
|
const transition = useMemo(() => {
|
|
return {
|
|
x: {
|
|
duration: shouldReveal ? duration : 0,
|
|
repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
|
|
ease: [0.42, 0, 0.58, 1],
|
|
delay: shouldReveal ? delay + revealDelay : 0,
|
|
},
|
|
y: {
|
|
duration: shouldReveal ? duration : 0,
|
|
repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
|
|
ease: [0.42, 0, 0.58, 1],
|
|
delay: shouldReveal ? delay + revealDelay : 0,
|
|
},
|
|
scale: {
|
|
duration: shouldReveal ? duration : 0.8,
|
|
repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
|
|
ease: shouldReveal ? [0.42, 0, 0.58, 1] : [0, 0, 0.58, 1],
|
|
delay: shouldReveal ? delay + revealDelay : revealDelay,
|
|
},
|
|
opacity: {
|
|
duration: 1.2,
|
|
ease: [0, 0, 0.58, 1],
|
|
delay: shouldReveal ? revealDelay : 0,
|
|
},
|
|
} as any;
|
|
}, [shouldReveal, duration, delay, revealDelay]);
|
|
|
|
return (
|
|
<motion.div
|
|
className="absolute rounded-full"
|
|
style={style}
|
|
initial={initial}
|
|
animate={animate}
|
|
transition={transition}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const MemoFloatingOrb = memo(FloatingOrb);
|
|
|
|
export function FloatingOrbs() {
|
|
const { orbsRevealed } = useOnboarding();
|
|
const reduceMotion = useReducedMotion();
|
|
const [mounted, setMounted] = useState(false);
|
|
const [orbs, setOrbs] = useState<Array<{
|
|
id: number;
|
|
size: number;
|
|
initialX: number;
|
|
initialY: number;
|
|
duration: number;
|
|
delay: number;
|
|
revealDelay: number;
|
|
color: {
|
|
primary: string;
|
|
secondary: string;
|
|
tertiary: string;
|
|
};
|
|
}>>([]);
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
|
|
const screenWidth = typeof window !== "undefined" ? window.innerWidth : 1200;
|
|
const screenHeight = typeof window !== "undefined" ? window.innerHeight : 800;
|
|
|
|
// Define edge zones (avoiding center)
|
|
const edgeThickness = Math.min(screenWidth, screenHeight) * 0.25; // 25% of smaller dimension
|
|
|
|
// Define rainbow color palette
|
|
const colorPalette = [
|
|
{ // Magenta
|
|
primary: "rgba(255, 0, 150, 0.6)",
|
|
secondary: "rgba(255, 100, 200, 0.4)",
|
|
tertiary: "rgba(255, 150, 220, 0.1)"
|
|
},
|
|
{ // Yellow
|
|
primary: "rgba(255, 235, 59, 0.6)",
|
|
secondary: "rgba(255, 245, 120, 0.4)",
|
|
tertiary: "rgba(255, 250, 180, 0.1)"
|
|
},
|
|
{ // Light Blue
|
|
primary: "rgba(100, 181, 246, 0.6)",
|
|
secondary: "rgba(144, 202, 249, 0.4)",
|
|
tertiary: "rgba(187, 222, 251, 0.1)"
|
|
},
|
|
{ // Orange (keeping original)
|
|
primary: "rgba(255, 154, 0, 0.6)",
|
|
secondary: "rgba(255, 206, 84, 0.4)",
|
|
tertiary: "rgba(255, 154, 0, 0.1)"
|
|
},
|
|
{ // Very Light Red/Pink
|
|
primary: "rgba(255, 138, 128, 0.6)",
|
|
secondary: "rgba(255, 171, 145, 0.4)",
|
|
tertiary: "rgba(255, 205, 210, 0.1)"
|
|
}
|
|
];
|
|
|
|
// Generate orb configurations positioned along edges
|
|
const newOrbs = Array.from({ length: 8 }, (_, i) => {
|
|
let x: number;
|
|
let y: number;
|
|
const zone = i % 4; // Rotate through 4 zones: top, right, bottom, left
|
|
|
|
switch (zone) {
|
|
case 0: // Top edge
|
|
x = Math.random() * screenWidth;
|
|
y = Math.random() * edgeThickness;
|
|
break;
|
|
case 1: // Right edge
|
|
x = screenWidth - edgeThickness + Math.random() * edgeThickness;
|
|
y = Math.random() * screenHeight;
|
|
break;
|
|
case 2: // Bottom edge
|
|
x = Math.random() * screenWidth;
|
|
y = screenHeight - edgeThickness + Math.random() * edgeThickness;
|
|
break;
|
|
case 3: // Left edge
|
|
x = Math.random() * edgeThickness;
|
|
y = Math.random() * screenHeight;
|
|
break;
|
|
default:
|
|
x = Math.random() * screenWidth;
|
|
y = Math.random() * screenHeight;
|
|
}
|
|
|
|
return {
|
|
id: i,
|
|
size: Math.random() * 300 + 200, // 200px to 500px
|
|
initialX: x,
|
|
initialY: y,
|
|
duration: Math.random() * 20 + 15, // 15-35 seconds (longer for more gentle movement)
|
|
delay: i * 0.4, // Staggered start for floating animation
|
|
revealDelay: i * 0.2, // Faster staggered reveal
|
|
color: colorPalette[i % colorPalette.length]!, // Cycle through rainbow colors
|
|
};
|
|
});
|
|
|
|
setOrbs(newOrbs);
|
|
}, []);
|
|
|
|
if (!mounted || orbs.length === 0) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 pointer-events-none overflow-hidden"
|
|
style={{ isolation: "isolate", contain: "paint" }}
|
|
>
|
|
{orbs.map((orb) => (
|
|
<MemoFloatingOrb
|
|
key={orb.id}
|
|
size={orb.size}
|
|
initialX={orb.initialX}
|
|
initialY={orb.initialY}
|
|
duration={reduceMotion ? 0 : orb.duration}
|
|
delay={orb.delay}
|
|
revealDelay={orb.revealDelay}
|
|
shouldReveal={reduceMotion ? false : orbsRevealed}
|
|
color={orb.color}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|