feat: redesign storage page with interactive components and dark theme

- Added interactive architecture section with tabbed navigation and smooth transitions
- Implemented horizontal scrolling capabilities carousel with image backgrounds
- Updated call-to-action section with bordered container layout and improved button styling
- Replaced core value section with animated self-healing storage features
- Applied consistent dark theme (#111111, #121212) with cyan accents across all storage components
This commit is contained in:
2025-11-07 22:28:03 +01:00
parent 0b6bcfedd0
commit 451c1f5c56
13 changed files with 718 additions and 120 deletions

BIN
public/images/encrypted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

BIN
public/images/ipfs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

BIN
public/images/s3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 KiB

View File

@@ -106,7 +106,7 @@ export const H5 = createTextComponent(
)
export const Eyebrow = createTextComponent(
'h2',
'text-base/7 font-semibold tracking-[0.18em] uppercase',
'text-base/7 font-semibold uppercase tracking-[0.16em]',
{ color: 'accent' }
)
export const SectionHeader = createTextComponent(

View File

@@ -1,48 +1,66 @@
import { CircleBackground } from '../../components/CircleBackground'
import { Container } from '@/components/Container'
import { Button } from '@/components/Button'
"use client";
import { Container } from "@/components/Container";
import { Button } from "@/components/Button";
export function CallToAction() {
return (
<section
id="get-started"
className="relative overflow-hidden bg-gray-900 py-20 sm:py-28"
>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<CircleBackground color="#06b6d4" className="animate-spin-slower" />
</div>
<Container className="relative">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl lg:text-4xl font-medium tracking-tight text-white sm:text-4xl">
Choose How You Want to Start
</h2>
<p className="mt-6 text-lg text-gray-300">
Store data in your Mycelium Cloud environment
or host your own node for full sovereignty.
<section className="relative overflow-hidden bg-gray-900">
{/* ✅ Top horizontal line with spacing */}
<div className="max-w-7xl mx-auto py-6 border border-t-0 border-b-0 border-gray-700"></div>
<div className="w-full border-t border-l border-r border-gray-700" />
</p>
<div className="mt-10 flex flex-wrap justify-center gap-x-6 gap-y-4">
<Button
to="https://myceliumcloud.tf"
as="a"
variant="solid"
color="white"
target="_blank"
rel="noreferrer"
>
Use Storage in Cloud
</Button>
<Button
to="#storage-developer-experience"
as="a"
variant="outline"
color="white"
>
Host a Node
</Button>
{/* ✅ Main boxed area */}
<div
id="get-started"
className="relative py-18 max-w-7xl mx-auto bg-[#090909] border border-t-0 border-b-0 border-gray-700"
>
<Container className="relative">
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-3xl lg:text-4xl font-medium tracking-tight text-white sm:text-4xl">
Choose How You Want to Start
</h2>
<p className="mt-6 text-lg text-gray-300">
Store data in your Mycelium Cloud environment
or host your own node for full sovereignty.
</p>
{/* ✅ Two cards, stacked center with spacing */}
<div className="mt-8 flex flex-wrap justify-center gap-x-10 gap-y-8">
<div className="flex flex-col items-center text-center max-w-xs">
<Button
to="https://myceliumcloud.tf"
as="a"
variant="solid"
color="cyan"
className="mt-4"
target="_blank"
rel="noreferrer"
>
Use Storage in Cloud
</Button>
</div>
<div className="flex flex-col items-center text-center max-w-xs">
<Button
to="#storage-developer-experience"
as="a"
variant="outline"
color="white"
className="mt-4"
>
Host a Node
</Button>
</div>
</div>
</div>
</div>
</Container>
</Container>
</div>
{/* ✅ Bottom horizontal line with spacing */}
<div className="w-full border-b border-gray-700" />
<div className="max-w-7xl mx-auto py-6 border border-t-0 border-b-0 border-gray-700 bg-transparent" />
</section>
)
);
}

View File

@@ -1,54 +1,81 @@
import { Container } from '@/components/Container'
import { Eyebrow, SectionHeader, P } from '@/components/Texts'
"use client";
import { useState } from "react";
import { Container } from "@/components/Container";
import { Eyebrow, SectionHeader, P, H3, H4, H5, CT, CP } from "@/components/Texts";
const architecture = [
{
title: 'Encrypted Storage Substrate',
description: 'Keeps data private and verifiable.',
title: "Encrypted Storage Substrate",
description: "Keeps data private and verifiable.",
},
{
title: 'Mesh Routing Layer',
description: 'Connects clients and workloads securely, anywhere.',
title: "Mesh Routing Layer",
description: "Connects clients and workloads securely, anywhere.",
},
{
title: 'Protocol Gateway Layer',
title: "Protocol Gateway Layer",
description:
'Serve the same dataset over S3, IPFS, WebDAV, or HTTP.',
"Serve the same dataset over S3, IPFS, WebDAV, or HTTP.",
},
]
];
export function StorageArchitecture() {
const [active, setActive] = useState(0);
return (
<section className="bg-gray-50 py-24 sm:py-32">
<Container>
<section className="bg-[#121212] w-full max-w-8xl mx-auto">
{/* ✅ Top horizontal line with spacing */}
<div className="max-w-7xl mx-auto py-6 border border-t-0 border-b-0 border-gray-800" />
<div className="w-full border-t border-l border-r border-gray-800" />
{/* ✅ Boxed container */}
<Container className="bg-[#111111] w-full max-w-7xl mx-auto py-12 border border-t-0 border-b-0 border-gray-800">
<div className="mx-auto max-w-3xl text-center">
<Eyebrow>ARCHITECTURE</Eyebrow>
<SectionHeader as="h2" className="mt-6 text-gray-900">
HOW IT WORKS
</SectionHeader>
<P className="mt-6 text-gray-600">
<H3 className="mt-4 text-white">
How it Works
</H3>
<P className="mt-6 text-gray-400">
A layered design that encrypts, routes, and exposes storage through
multiple protocols without duplicating data or compromising
sovereignty.
</P>
</div>
<div className="mx-auto mt-16 max-w-4xl space-y-6">
{architecture.map((item) => (
{/* ✅ New 2-column layout */}
<div className="mx-auto mt-8 grid max-w-5xl grid-cols-1 gap-2 lg:grid-cols-3 bg-[#121212] ">
{/* LEFT — 1 column (3 rows) */}
<div className="space-y-2">
{architecture.map((item, index) => (
<button
key={item.title}
className={`w-full border bg-[#171717] text-left border-white/10 p-4 backdrop-blur-sm transition hover:-translate-y-1 hover:border-cyan-300/50 hover:bg-white/8
${active === index
? "border-cyan-400 shadow-md"
: "border-gray-800 hover:border-cyan-200 hover:shadow-sm"}`}
onClick={() => setActive(index)}
>
<CP className="text-white font-semibold">{item.title}</CP>
</button>
))}
</div>
{/* RIGHT — 2 columns */}
<div className="lg:col-span-2 flex items-center justify-center border border-gray-800 bg-[#171717] p-10 backdrop-blur-sm transition hover:-translate-y-1 hover:border-cyan-300/50 hover:bg-white/8" >
<div
key={item.title}
className="rounded-3xl border border-slate-200 bg-white p-8 shadow-sm transition hover:-translate-y-1 hover:border-cyan-300 hover:shadow-lg"
key={active} // ✅ force smooth transition
className="transition-opacity duration-300 opacity-100 animate-fade"
>
<h3 className="text-xl font-semibold text-gray-900">
{item.title}
</h3>
<p className="mt-3 text-sm leading-relaxed text-gray-600">
{item.description}
</p>
<H5 className="text-white">{architecture[active].title}</H5>
<P className="mt-2 text-gray-400 max-w-xl">
{architecture[active].description}
</P>
</div>
))}
</div>
</div>
</Container>
<div className="w-full border-b border-gray-800 bg-[#121212]" />
<div className="max-w-7xl mx-auto py-6 border border-t-0 border-b-0 border-gray-800" />
</section>
)
);
}

View File

@@ -0,0 +1,110 @@
"use client";
import { useRef } from "react";
import { Eyebrow, CP, CT, H5 } from "@/components/Texts";
import { IoArrowBackOutline, IoArrowForwardOutline } from "react-icons/io5";
const capabilities = [
{
isIntro: true,
eyebrow: "CAPABILITIES",
title: "Flexible, Resilient, and Controllable Storage",
description:
"Mycelium Storage is designed for modern data workloads, providing a range of access methods and control over data placement.",
},
{
title: "S3-Compatible Object Storage",
description: "Works with existing SDKs & tooling.",
imageUrl: "/images/s3.png",
},
{
title: "IPFS & Content-Addressed Access",
description: "Ideal for distributed and decentralized workloads.",
imageUrl: "/images/ipfs.png",
},
{
title: "Filesystem Mounts (WebDAV / POSIX)",
description: "Mount storage directly into workflows and apps.",
imageUrl: "/images/filesystem.png",
},
{
title: "Encrypted Replication & Placement Control",
description: "Choose data's ownership and locations.",
imageUrl: "/images/encrypted.png",
},
];
export function StorageCapabilitiesNew() {
const sliderRef = useRef<HTMLUListElement>(null);
const scrollLeft = () => sliderRef.current?.scrollBy({ left: -400, behavior: "smooth" });
const scrollRight = () => sliderRef.current?.scrollBy({ left: 400, behavior: "smooth" });
return (
<section className="bg-[#121212] w-full max-w-8xl mx-auto">
<div className="max-w-7xl mx-auto py-6 border border-t-0 border-b-0 border-gray-800" />
<div className="w-full border-t border-l border-r border-gray-800" />
<div className="relative mx-auto max-w-7xl border border-t-0 border-b-0 border-gray-800 bg-[#111111] overflow-hidden">
{/* Horizontal Slider — shows part of next card */}
<ul
ref={sliderRef}
className="flex overflow-x-auto snap-x snap-mandatory scroll-smooth no-scrollbar"
>
{capabilities.map((item, idx) => (
<li
key={idx}
className={`snap-start shrink-0 w-[85%] sm:w-[50%] lg:w-[33%] border border-gray-800 relative ${item.isIntro ? ' p-10' : 'bg-cover bg-center'}`}
style={item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : {}}
>
<div className={`relative z-10 flex flex-col h-full ${item.isIntro ? 'justify-between' : 'justify-end'}`}>
{/* First card with arrows */}
{item.isIntro ? (
<div className="flex flex-col justify-between h-full ">
<div>
<Eyebrow className="">{item.eyebrow}</Eyebrow>
<H5 className="text-white mt-4 lg:text-2xl text-xl">{item.title}</H5>
<p className="mt-4 text-gray-400 lg:text-lg text-sm leading-relaxed">{item.description}</p>
</div>
{/* Arrows inside first card */}
<div className="flex items-center gap-x-4 mt-2">
<a
href="#"
className="inline-flex items-center gap-1 text-cyan-400 hover:text-cyan-300 text-sm font-medium mr-auto"
>
Learn more
</a>
<button
onClick={scrollLeft}
className="h-8 w-8 flex items-center justify-center border border-gray-700 rounded-md hover:border-cyan-500 transition-colors"
>
<IoArrowBackOutline className="text-gray-300" size={16} />
</button>
<button
onClick={scrollRight}
className="h-8 w-8 flex items-center justify-center border border-gray-700 rounded-md hover:border-cyan-500 transition-colors"
>
<IoArrowForwardOutline className="text-gray-300" size={16} />
</button>
</div>
</div>
) : (
<div className="bg-[#111111] p-6 h-20 flex flex-col justify-center border-t border-b-0 border-gray-800">
<p className="text-base font-semibold text-white">{item.title}</p>
<p className="mt-2 text-gray-400 leading-snug">{item.description}</p>
</div>
)}
</div>
</li>
))}
</ul>
</div>
<div className="w-full border-b border-gray-800" />
<div className="max-w-7xl mx-auto py-6 border border-t-0 border-b-0 border-gray-800" />
</section>
);
}

View File

@@ -1,20 +1,37 @@
"use client";
import { Container } from "@/components/Container";
import { H3, P, Eyebrow } from "@/components/Texts";
import { H3, P, Eyebrow, CT, CP } from "@/components/Texts";
import Encrypted from "./animation/Encrypted";
import SelfHealing from "./animation/SelfHealing";
import Residency from "./animation/Residency";
export function StorageCoreValue() {
const logos = [
{ src: '/images/logo/cryptpad.png', href: 'https://cryptpad.fr' },
{ src: '/images/logo/gitea.png', href: 'https://about.gitea.com' },
{ src: '/images/logo/lifekit.png', href: '#' }, // No link available
{ src: '/images/logo/matrix.png', href: 'https://matrix.org' },
{ src: '/images/logo/nextcloud.png', href: 'https://nextcloud.com' },
{ src: '/images/logo/stalwart.png', href: 'https://stalw.art' },
];
const values = [
{ id: "Encrypted",
title: "Encrypted and verifiable at rest and in motion",
href: "#",
animation: Encrypted,
},
{ id: "SelfHealing",
title: "Self-healing replication and integrity checks",
href: "#",
animation: SelfHealing,
},
{ id: "Residency",
title: "Residency + governance policies you actually control",
href: "#",
animation: Residency,
},
]
return (
<section className="w-full max-w-8xl mx-auto bg-transparent">
{/* ✅ Top horizontal line with spacing */}
<div className="max-w-7xl bg-transparent mx-auto py-6 border border-t-0 border-b-0 border-gray-100"></div>
<div className="w-full border-t border-l border-r border-gray-100" />
{/* ✅ Boxed container */}
<div className="max-w-7xl bg-white mx-auto py-12 border border-t-0 border-b-0 border-gray-100">
@@ -23,31 +40,29 @@ export function StorageCoreValue() {
<Eyebrow className="text-cyan-500">Featured Blueprint</Eyebrow>
<H3 className="text-3xl lg:text-4xl font-medium tracking-tight text-gray-900 mt-2">
Your Personal Sovereign Cloud Workspace
Sovereign Storage That Heals Itself
</H3>
<P className="mt-6 text-lg text-gray-600">
Digital Me is an example environment built to demonstrate whats possible on top of the Mycelium Stack a full personal cloud you can deploy, customize, or extend. Your files, communication, apps, and optional AI agent, all running privately on infrastructure you choose.
Mycelium Storage continuously verifies integrity and restores replicas automatically, so data stays available without operational overhead.
</P>
</div>
{/* ✅ 3x2 logo grid */}
<div className="mt-12 grid grid-cols-3 gap-x-8 gap-y-12">
{logos.map((logo, i) => (
<div key={i} className="flex justify-center">
<a
href={logo.href}
target="_blank"
rel="noopener noreferrer"
className="transition-transform duration-300 hover:scale-105"
>
<img
src={logo.src}
alt={`Logo ${i + 1}`}
className="max-h-12 w-auto object-contain"
/>
</a>
</div>
{values.map((value, i) => (
<a
key={i}
href={value.href}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center text-center p-6 border border-gray-200 rounded-lg transition-transform duration-300 hover:scale-105 hover:shadow-lg"
>
<value.animation />
<CT className="text-gray-900 mt-4 font-semibold">
{value.title}
</CT>
</a>
))}
</div>
</Container>

View File

@@ -1,4 +1,3 @@
import { Container } from '@/components/Container'
import { Eyebrow, SectionHeader, P, Small } from '@/components/Texts'
const highlights = [
@@ -24,27 +23,17 @@ const highlights = [
export function StorageOverview() {
return (
<section className="bg-gray-950 py-24 sm:py-32">
<Container>
<section className="bg-[#121212] w-full max-w-8xl mx-auto">
{/* ✅ Top horizontal line with spacing */}
<div className="max-w-7xl mx-auto border border-t-0 border-b-0 border-gray-800" />
<div className="w-full border-t border-l border-r border-gray-800" />
<div className="mx-auto max-w-3xl text-center">
<Eyebrow className="tracking-[0.32em] uppercase text-cyan-300">
Platform Overview
</Eyebrow>
<SectionHeader as="h2" color="light" className="mt-6 font-medium">
Core Features
</SectionHeader>
<P color="lightSecondary" className="mt-6">
Built on sovereign infrastructure, Mycelium Storage keeps data
resilient, verifiable, and instantly accessible. Encryption,
replication, and governance are woven directly into the substrate.
</P>
</div>
<div className="mt-16 grid gap-8 lg:grid-cols-3">
<div className="bg-[#121212] w-full max-w-7xl mx-auto border border-t-0 border-b-0 border-gray-800">
<div className="grid lg:grid-cols-3">
{highlights.map((item) => (
<div
key={item.title}
className="group relative overflow-hidden rounded-3xl border border-white/10 bg-white/4 p-8 backdrop-blur-sm transition hover:-translate-y-1 hover:border-cyan-300/50 hover:bg-white/8"
className="group relative overflow-hidden border border-white/10 bg-white/4 p-8 backdrop-blur-sm transition hover:border-cyan-300/50 hover:bg-white/8"
>
<div className="absolute inset-0 bg-linear-to-br from-cyan-500/0 via-white/5 to-cyan-300/20 opacity-0 transition group-hover:opacity-100" />
<div className="relative">
@@ -61,7 +50,9 @@ export function StorageOverview() {
</div>
))}
</div>
</Container>
</div>
<div className="w-full border-b border-gray-800 bg-[#121212]" />
<div className="max-w-7xl mx-auto py-6 border border-t-0 border-b-0 border-gray-800" />
</section>
)
}

View File

@@ -4,8 +4,8 @@ import { StorageOverview } from './StorageOverview'
import { StorageArchitecture } from './StorageArchitecture'
import { StorageUseCases } from './StorageUseCases'
import { CallToAction } from './CallToAction'
import { StorageCapabilities } from './StorageCapabilities'
import { StorageDesign } from './StorageDesign'
import { StorageCapabilitiesNew } from './StorageCapabilitiesNew'
import { StorageCoreValue } from './StorageCoreValue'
export default function StoragePage() {
return (
@@ -15,11 +15,11 @@ export default function StoragePage() {
</AnimatedSection>
<AnimatedSection>
<StorageCapabilities />
<StorageCapabilitiesNew />
</AnimatedSection>
<AnimatedSection>
<StorageDesign />
<StorageCoreValue />
</AnimatedSection>
<AnimatedSection>

View File

@@ -0,0 +1,223 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import clsx from "clsx";
type Props = {
className?: string;
accent?: string;
gridStroke?: string;
stroke?: string;
};
const W = 760;
const H = 420;
export default function Residency({
className,
accent = "#00b8db",
gridStroke = "#e5e7eb",
stroke = "#111111",
}: Props) {
const prefers = useReducedMotion();
// Layout: central governance node + 3 regional nodes
const center = { x: 380, y: 200 };
const regions = [
{ x: 220, y: 120 },
{ x: 540, y: 120 },
{ x: 380, y: 300 },
];
// Path for data transfer (circular motion between regions)
const flowPath = `M ${regions[0].x} ${regions[0].y}
C 300 80, 460 80, ${regions[1].x} ${regions[1].y}
C 480 160, 420 260, ${regions[2].x} ${regions[2].y}
C 340 260, 280 160, ${regions[0].x} ${regions[0].y} Z`;
return (
<div
className={clsx("relative overflow-hidden", className)}
aria-hidden="true"
role="img"
style={{ background: "transparent" }}
>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
{/* ✅ Subtle light grid (same as Encrypted.tsx) */}
<defs>
<pattern id="grid-residency" width="28" height="28" patternUnits="userSpaceOnUse">
<path
d="M 28 0 L 0 0 0 28"
fill="none"
stroke={gridStroke}
strokeWidth="1"
opacity="0.4"
/>
</pattern>
<filter id="res-glow">
<feGaussianBlur stdDeviation="3" result="b" />
<feMerge>
<feMergeNode in="b" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<rect width={W} height={H} fill="url(#grid-residency)" />
{/* ✅ Base dotted jurisdiction boundary */}
<circle
cx={center.x}
cy={center.y}
r={140}
fill="none"
stroke={stroke}
strokeWidth={2}
strokeDasharray="6 6"
opacity="0.3"
/>
{/* ✅ Cyan policy ring expanding + fading */}
{!prefers && (
<motion.circle
cx={center.x}
cy={center.y}
r={140}
stroke={accent}
strokeWidth={2}
fill="none"
initial={{ scale: 0.9, opacity: 0 }}
animate={{
scale: [1, 1.15, 1.3],
opacity: [0.15, 0.4, 0],
}}
transition={{
duration: 2.8,
repeat: Infinity,
ease: [0.22, 1, 0.36, 1],
}}
filter="url(#res-glow)"
/>
)}
{/* ✅ Cyan glow radius (policy control zone) */}
{!prefers && (
<motion.circle
cx={center.x}
cy={center.y}
r={60}
fill={accent}
opacity={0.08}
initial={{ scale: 1, opacity: 0.05 }}
animate={{
opacity: [0.05, 0.15, 0.05],
scale: [1, 1.05, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: [0.22, 1, 0.36, 1],
}}
/>
)}
{/* ✅ Central governance node */}
<circle
cx={center.x}
cy={center.y}
r={28}
fill="#fff"
stroke={stroke}
strokeWidth={2}
/>
<circle
cx={center.x}
cy={center.y}
r={12}
fill={accent}
stroke={stroke}
strokeWidth={2}
filter="url(#res-glow)"
/>
{/* ✅ Regional nodes */}
{regions.map((r, i) => (
<g key={i}>
<circle
cx={r.x}
cy={r.y}
r={22}
fill="#fff"
stroke={stroke}
strokeWidth={2}
/>
<circle
cx={r.x}
cy={r.y}
r={10}
fill="none"
stroke={stroke}
strokeWidth={1.5}
opacity="0.6"
/>
</g>
))}
{/* ✅ Data transfer flow (light dotted path) */}
<motion.path
d={flowPath}
fill="none"
stroke={stroke}
strokeWidth={1.5}
strokeDasharray="4 4"
opacity="0.3"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 1.4 }}
/>
{/* ✅ Cyan packet traveling within jurisdiction */}
{!prefers && (
<motion.circle
r={6}
fill={accent}
style={{ offsetPath: `path('${flowPath}')` }}
initial={{ offsetDistance: "0%", opacity: 0 }}
animate={{
offsetDistance: ["0%", "100%"],
opacity: [0.3, 1, 0.3],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear",
}}
filter="url(#res-glow)"
/>
)}
{/* ✅ Governance shield icon */}
<motion.path
d={`M ${center.x} ${center.y - 70}
L ${center.x + 20} ${center.y - 60}
L ${center.x + 16} ${center.y - 35}
L ${center.x} ${center.y - 25}
L ${center.x - 16} ${center.y - 35}
L ${center.x - 20} ${center.y - 60}
Z`}
fill="none"
stroke={accent}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
filter="url(#res-glow)"
/>
</svg>
</div>
);
}

View File

@@ -0,0 +1,214 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import clsx from "clsx";
type Props = {
className?: string;
accent?: string;
gridStroke?: string;
stroke?: string;
};
const W = 760;
const H = 420;
export default function SelfHealing({
className,
accent = "#00b8db",
gridStroke = "#e5e7eb",
stroke = "#111111",
}: Props) {
const prefers = useReducedMotion();
// diamond node layout
const nodes = [
{ x: 380, y: 130 }, // top
{ x: 240, y: 240 }, // left
{ x: 520, y: 240 }, // right
{ x: 380, y: 320 }, // bottom
];
// connection paths
const links = [
[0, 1],
[0, 2],
[1, 3],
[2, 3],
[1, 2],
];
// helper for path drawing
const drawLine = (i: number, j: number) => {
const a = nodes[i];
const b = nodes[j];
return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
};
return (
<div
className={clsx("relative overflow-hidden", className)}
aria-hidden="true"
role="img"
style={{ background: "transparent" }}
>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
{/* ✅ Subtle grid (same as Encrypted.tsx) */}
<defs>
<pattern id="grid-heal" width="28" height="28" patternUnits="userSpaceOnUse">
<path
d="M 28 0 L 0 0 0 28"
fill="none"
stroke={gridStroke}
strokeWidth="1"
opacity="0.4"
/>
</pattern>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="b" />
<feMerge>
<feMergeNode in="b" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<rect width={W} height={H} fill="url(#grid-heal)" />
{/* ✅ Static network links */}
{links.map(([i, j], idx) => (
<motion.path
key={idx}
d={drawLine(i, j)}
stroke={stroke}
strokeWidth={2}
strokeLinecap="round"
fill="none"
opacity="0.25"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.8, delay: idx * 0.1 }}
/>
))}
{/* ✅ Circulating health-check pulse */}
{!prefers &&
[0, 1, 2, 3].map((i) => {
const next = (i + 1) % 4;
const path = drawLine(i, next);
return (
<motion.circle
key={`pulse-${i}`}
r={5}
fill={accent}
style={{ offsetPath: `path('${path}')` }}
initial={{ offsetDistance: "0%", opacity: 0 }}
animate={{
offsetDistance: ["0%", "100%"],
opacity: [0.1, 0.9, 0.1],
}}
transition={{
duration: 2.8,
delay: i * 0.4,
repeat: Infinity,
ease: "linear",
}}
filter="url(#glow)"
/>
);
})}
{/* ✅ Nodes */}
{nodes.map((n, i) => (
<g key={`node-${i}`}>
<circle
cx={n.x}
cy={n.y}
r={26}
fill="none"
stroke={stroke}
strokeWidth={2}
opacity="0.8"
/>
<circle
cx={n.x}
cy={n.y}
r={12}
fill="#fff"
stroke={stroke}
strokeWidth={2}
/>
</g>
))}
{/* ✅ Simulated failure (bottom node flickers out, then heals) */}
{!prefers && (
<motion.circle
cx={nodes[3].x}
cy={nodes[3].y}
r={12}
fill="#fff"
stroke={stroke}
strokeWidth={2}
animate={{
opacity: [1, 0.2, 1, 1],
scale: [1, 0.9, 1, 1],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
/>
)}
{/* ✅ Healing pulse wave from neighbors → bottom node */}
{!prefers &&
[nodes[1], nodes[2]].map((n, i) => {
const path = `M ${n.x} ${n.y} L ${nodes[3].x} ${nodes[3].y}`;
return (
<motion.circle
key={`heal-${i}`}
r={5}
fill={accent}
style={{ offsetPath: `path('${path}')` }}
initial={{ offsetDistance: "0%", opacity: 0 }}
animate={{
offsetDistance: ["0%", "100%"],
opacity: [0, 1, 0],
}}
transition={{
delay: 1.5 + i * 0.2,
duration: 1.8,
repeat: Infinity,
repeatDelay: 2.2,
ease: "linear",
}}
filter="url(#glow)"
/>
);
})}
{/* ✅ Integrity halo on healed node */}
{!prefers && (
<motion.circle
cx={nodes[3].x}
cy={nodes[3].y}
r={32}
fill="none"
stroke={accent}
strokeWidth={2}
initial={{ opacity: 0 }}
animate={{ opacity: [0.15, 0.4, 0.15], scale: [1, 1.12, 1] }}
transition={{
duration: 2.8,
repeat: Infinity,
ease: [0.22, 1, 0.36, 1],
}}
filter="url(#glow)"
/>
)}
</svg>
</div>
);
}