diff --git a/app/globals.css b/app/globals.css index de839ae..9800bf6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,38 +4,37 @@ @layer base { :root { - /* 3blue1brown-inspired dark palette: deep space blue background, - a warm gold accent for emphasis, and a small, deliberate set of - "state" colors used consistently across every visualization. */ - --background: 222 47% 7%; - --foreground: 210 40% 96%; + /* Monochrome chrome: the shell carries no color of its own, so the + five algorithm-state colors below are the only color on the page. */ + --background: 224 15% 6%; + --foreground: 210 20% 95%; - --primary: 199 89% 64%; /* 3b1b sky blue */ - --primary-foreground: 222 47% 7%; - --secondary: 222 30% 16%; - --secondary-foreground: 210 40% 90%; - --accent: 43 96% 62%; /* warm gold, used sparingly */ - --accent-foreground: 222 47% 7%; - --destructive: 358 75% 59%; - --destructive-foreground: 210 40% 96%; - --muted: 222 25% 14%; - --muted-foreground: 215 20% 65%; - --card: 222 40% 10%; - --card-foreground: 210 40% 96%; - --popover: 222 44% 9%; - --popover-foreground: 210 40% 96%; - --border: 222 25% 20%; - --input: 222 25% 20%; - --ring: 199 89% 64%; + --primary: 210 25% 96%; /* near-white, inverted CTA fill */ + --primary-foreground: 224 20% 9%; + --secondary: 224 12% 15%; + --secondary-foreground: 210 15% 90%; + --accent: 224 12% 17%; /* neutral hover surface, not a brand color */ + --accent-foreground: 210 20% 95%; + --destructive: 358 65% 55%; + --destructive-foreground: 210 20% 96%; + --muted: 224 12% 13%; + --muted-foreground: 216 10% 62%; + --card: 224 14% 9%; + --card-foreground: 210 20% 95%; + --popover: 224 16% 8%; + --popover-foreground: 210 20% 95%; + --border: 224 12% 18%; + --input: 224 12% 18%; + --ring: 205 90% 62%; /* the one deliberate accent: focus only */ --radius: 0.75rem; /* Algorithm state colors — one meaning each, used everywhere. */ - --state-idle: 199 60% 55%; - --state-compare: 43 96% 62%; + --state-idle: 205 40% 50%; + --state-compare: 43 90% 60%; --state-swap: 358 75% 59%; - --state-sorted: 152 60% 50%; - --state-pivot: 271 76% 68%; - --state-active-range: 199 89% 64%; + --state-sorted: 152 55% 48%; + --state-pivot: 271 70% 66%; + --state-active-range: 205 60% 55%; } } @@ -71,12 +70,6 @@ } @layer utilities { - .glow-primary { - box-shadow: 0 0 24px hsl(var(--primary) / 0.35); - } - .glow-accent { - box-shadow: 0 0 24px hsl(var(--accent) / 0.35); - } .text-balance { text-wrap: balance; } diff --git a/app/layout.tsx b/app/layout.tsx index 8160a02..feac195 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,22 +1,22 @@ import { ReactNode } from 'react'; -import { Inter, JetBrains_Mono } from 'next/font/google'; +import { Geist, Geist_Mono } from 'next/font/google'; import { cn } from '@/lib/utils'; import './globals.css'; -const fontSans = Inter({ +const fontSans = Geist({ subsets: ['latin'], display: 'swap', variable: '--font-sans', }); -const fontMono = JetBrains_Mono({ +const fontMono = Geist_Mono({ subsets: ['latin'], display: 'swap', variable: '--font-mono', }); export const metadata = { - title: 'Valgo — Watch how sorting actually works', + title: 'Valgo - Watch how sorting actually works', description: 'An animated, narrated visualizer for nine classic sorting algorithms.', }; diff --git a/app/page.tsx b/app/page.tsx index ce52ca1..5cd5af1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,7 +14,7 @@ import StatsPanel from "@/components/StatsPanel"; import PlaybackControls, { Speed } from "@/components/PlaybackControls"; import { AlgorithmPicker } from "@/components/Pickers"; import SiteFooter from "@/components/SiteFooter"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import Panel from "@/components/Panel"; const SPEED_MS: Record = { slow: 260, normal: 120, fast: 55, turbo: 18 }; @@ -22,11 +22,27 @@ function randomArray(size: number): number[] { return Array.from({ length: size }, () => Math.floor(Math.random() * 100) + 1); } +// Deterministic so the server-rendered array matches the client's first +// render; the real random array is generated client-side after mount. +function seededArray(size: number): number[] { + return Array.from({ length: size }, (_, i) => ((i * 37) % 100) + 1); +} + +function BrandMark(props: React.SVGProps) { + return ( + + + + + + ); +} + export default function Page() { const [algorithm, setAlgorithm] = useState("bubble"); const [language, setLanguage] = useState("en"); const [arraySize, setArraySize] = useState(24); - const [baseArray, setBaseArray] = useState(() => randomArray(24)); + const [baseArray, setBaseArray] = useState(() => seededArray(24)); const [steps, setSteps] = useState([]); const [stepIndex, setStepIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); @@ -34,6 +50,11 @@ export default function Page() { const timeoutRef = useRef | null>(null); + useEffect(() => { + setBaseArray(randomArray(arraySize)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // (Re)compute the full step-by-step story whenever the algorithm or the // underlying data changes. Everything downstream just plays it back. useEffect(() => { @@ -111,94 +132,95 @@ export default function Page() { return (
-
-
-
-

- {t(language, "brand")} -

-

{t(language, "tagline")}

+
+
+
+ +

{t(language, "brand")}

+ + {t(language, "tagline")} +
-
- + - - - -
-
- - +
+ -
- +
+
+
+ + +
- {description && ( - - - {description.title} - -
{description.timeComplexity}
-
{description.spaceComplexity}
-
-
- -

{description.description}

-
-
- )} +
+ -
-

{t(language, "growthTitle")}

-

{t(language, "growthCaption")}

- + {description && ( + +

{description.title}

+
+
{description.timeComplexity}
+
{description.spaceComplexity}
+
+

{description.description}

+
+ )} + + +

{t(language, "growthTitle")}

+

{t(language, "growthCaption")}

+ +
diff --git a/components/Bars.tsx b/components/Bars.tsx index 64dd139..ca5cd41 100644 --- a/components/Bars.tsx +++ b/components/Bars.tsx @@ -36,7 +36,7 @@ function colorFor( if (action === "swap" || action === "shift" || action === "merge-write" || action === "overwrite") return stateColor.swap; return stateColor.compare; } - if (range && i >= range[0] && i <= range[1]) return "hsl(var(--state-range) / 0.55)"; + if (range && i >= range[0] && i <= range[1]) return "hsl(var(--state-active-range) / 0.55)"; return stateColor.idle; } diff --git a/components/Narration.tsx b/components/Narration.tsx index 1362811..e0ecec2 100644 --- a/components/Narration.tsx +++ b/components/Narration.tsx @@ -1,6 +1,7 @@ "use client"; import { AnimatePresence, motion } from "framer-motion"; +import Panel from "@/components/Panel"; interface NarrationProps { text: string; @@ -10,7 +11,7 @@ interface NarrationProps { export default function Narration({ text, stepIndex, title }: NarrationProps) { return ( -
+

{title}

@@ -26,6 +27,6 @@ export default function Narration({ text, stepIndex, title }: NarrationProps) {
-
+ ); } diff --git a/components/Panel.tsx b/components/Panel.tsx new file mode 100644 index 0000000..629947c --- /dev/null +++ b/components/Panel.tsx @@ -0,0 +1,11 @@ +import { HTMLAttributes } from "react"; +import { cn } from "@/lib/utils"; + +export default function Panel({ className, ...props }: HTMLAttributes) { + return ( +
+ ); +} diff --git a/components/PlaybackControls.tsx b/components/PlaybackControls.tsx index 0316f79..0f5fa40 100644 --- a/components/PlaybackControls.tsx +++ b/components/PlaybackControls.tsx @@ -3,6 +3,7 @@ import { Play, Pause, Shuffle, RotateCcw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Slider } from "@/components/ui/slider"; +import Panel from "@/components/Panel"; export type Speed = "slow" | "normal" | "fast" | "turbo"; @@ -38,7 +39,7 @@ export default function PlaybackControls({ }; return ( -
+
-
+ ); } diff --git a/components/SiteFooter.tsx b/components/SiteFooter.tsx index 2cdd50d..5272739 100644 --- a/components/SiteFooter.tsx +++ b/components/SiteFooter.tsx @@ -40,7 +40,7 @@ interface SiteFooterProps { export default function SiteFooter({ year, rightsLabel, licenseLabel, viewLicenseLabel, lang, onLangChange }: SiteFooterProps) { return (