Initial commit: MycoStack.xyz website
Epic single-page scrolling site merging Commons Stack ethos with mycelial design principles. Features scroll-driven dark-to-light color transitions, procedural mycelial canvas animation, anastomosis SVG animation, and an interactive network map of connected domains. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
4ec0f386f1
|
|
@ -0,0 +1,37 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN pnpm build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/out /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
/* Scroll-driven dynamic colors (set by JS) */
|
||||
--scroll-bg: oklch(0.08 0.02 30);
|
||||
--scroll-fg: oklch(0.85 0.03 80);
|
||||
--scroll-accent: oklch(0.45 0.12 60);
|
||||
--scroll-depth: 0;
|
||||
|
||||
/* Static palette tokens */
|
||||
--soil-black: oklch(0.08 0.02 30);
|
||||
--earth-brown: oklch(0.12 0.04 40);
|
||||
--compost-amber: oklch(0.55 0.15 45);
|
||||
--forest-deep: oklch(0.16 0.05 150);
|
||||
--mycelium-green: oklch(0.55 0.18 155);
|
||||
--mycelium-glow: oklch(0.72 0.20 135);
|
||||
--undernet-dark: oklch(0.22 0.04 200);
|
||||
--undernet-teal: oklch(0.50 0.14 220);
|
||||
--emergence-gold: oklch(0.60 0.16 90);
|
||||
--canopy-light: oklch(0.94 0.02 110);
|
||||
--mycopunk-orange: oklch(0.60 0.16 55);
|
||||
--mycopunk-warm: oklch(0.50 0.13 40);
|
||||
|
||||
/* shadcn-compatible tokens */
|
||||
--background: var(--scroll-bg);
|
||||
--foreground: var(--scroll-fg);
|
||||
--primary: var(--scroll-accent);
|
||||
--primary-foreground: oklch(0.98 0.01 80);
|
||||
--muted: oklch(0.20 0.02 30);
|
||||
--muted-foreground: oklch(0.60 0.02 80);
|
||||
--border: oklch(0.25 0.03 30);
|
||||
--input: oklch(0.25 0.03 30);
|
||||
--ring: var(--scroll-accent);
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-geist-sans), system-ui, sans-serif;
|
||||
--font-serif: var(--font-crimson-pro), Georgia, serif;
|
||||
--font-mono: var(--font-geist-mono), monospace;
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-soil-black: var(--soil-black);
|
||||
--color-earth-brown: var(--earth-brown);
|
||||
--color-compost-amber: var(--compost-amber);
|
||||
--color-forest-deep: var(--forest-deep);
|
||||
--color-mycelium-green: var(--mycelium-green);
|
||||
--color-mycelium-glow: var(--mycelium-glow);
|
||||
--color-undernet-dark: var(--undernet-dark);
|
||||
--color-undernet-teal: var(--undernet-teal);
|
||||
--color-emergence-gold: var(--emergence-gold);
|
||||
--color-canopy-light: var(--canopy-light);
|
||||
--color-mycopunk-orange: var(--mycopunk-orange);
|
||||
--color-mycopunk-warm: var(--mycopunk-warm);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--scroll-bg, oklch(0.08 0.02 30));
|
||||
color: var(--scroll-fg, oklch(0.85 0.03 80));
|
||||
transition: background-color 0.05s linear, color 0.05s linear;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
/* Section scroll-reveal */
|
||||
.section-reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.9s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.section-reveal.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Noise texture overlay */
|
||||
.noise-overlay {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.noise-overlay::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
opacity: 0.03;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: overlay;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Hero title emergence animation */
|
||||
@keyframes emerge-letter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
filter: blur(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.emerge-letter {
|
||||
animation: emerge-letter 1s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Scroll indicator pulse */
|
||||
@keyframes scroll-hint {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-hint {
|
||||
animation: scroll-hint 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Mycelial divider between sections */
|
||||
.mycelial-divider {
|
||||
width: 2px;
|
||||
height: 80px;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
var(--scroll-accent),
|
||||
transparent
|
||||
);
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
/* Anastomosis SVG line drawing */
|
||||
.draw-line {
|
||||
stroke-dasharray: 600;
|
||||
stroke-dashoffset: 600;
|
||||
}
|
||||
|
||||
.draw-line.animate {
|
||||
animation: draw-path 2.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes draw-path {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Merge pulse */
|
||||
@keyframes merge-pulse {
|
||||
0%,
|
||||
40% {
|
||||
opacity: 0;
|
||||
r: 0;
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
r: 10;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
r: 7;
|
||||
}
|
||||
}
|
||||
|
||||
.merge-pulse {
|
||||
animation: merge-pulse 3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
/* Network node hover glow */
|
||||
.network-node {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.network-node:hover {
|
||||
filter: drop-shadow(0 0 12px var(--scroll-accent));
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Terminal cursor blink */
|
||||
@keyframes blink {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
/* Card glass effect */
|
||||
.glass-card {
|
||||
background: oklch(0.5 0 0 / 0.08);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid oklch(0.5 0 0 / 0.12);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
/* Link style for domain easter eggs */
|
||||
a.domain-link {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-underline-offset: 3px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
a.domain-link:hover {
|
||||
text-decoration-style: solid;
|
||||
color: var(--scroll-accent);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import type { Metadata } from "next"
|
||||
import { GeistSans } from "geist/font/sans"
|
||||
import { GeistMono } from "geist/font/mono"
|
||||
import { Crimson_Pro } from "next/font/google"
|
||||
import { ScrollProvider } from "@/components/scroll-provider"
|
||||
import "./globals.css"
|
||||
|
||||
const crimsonPro = Crimson_Pro({
|
||||
weight: ["400", "500", "600", "700"],
|
||||
subsets: ["latin"],
|
||||
variable: "--font-crimson-pro",
|
||||
display: "swap",
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "MycoStack — Technology-Augmented Commons",
|
||||
description:
|
||||
"A reboot of the Commons Stack, merging technology with mycelial principles for regenerative systems. Growing from beneath the surface.",
|
||||
metadataBase: new URL("https://mycostack.xyz"),
|
||||
icons: {
|
||||
icon: "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍄</text></svg>",
|
||||
},
|
||||
openGraph: {
|
||||
title: "MycoStack — Technology-Augmented Commons",
|
||||
description:
|
||||
"Growing regenerative systems from beneath the surface. Commons governance meets mycelial design.",
|
||||
url: "https://mycostack.xyz",
|
||||
siteName: "MycoStack",
|
||||
locale: "en_US",
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "MycoStack — Technology-Augmented Commons",
|
||||
description:
|
||||
"Growing regenerative systems from beneath the surface.",
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body
|
||||
className={`${GeistSans.variable} ${GeistMono.variable} ${crimsonPro.variable} font-sans antialiased`}
|
||||
>
|
||||
<ScrollProvider>{children}</ScrollProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { MycelialCanvas } from "@/components/mycelial-canvas"
|
||||
import { HeroSection } from "@/components/hero-section"
|
||||
import { CompostSection } from "@/components/compost-section"
|
||||
import { MyceliumSection } from "@/components/mycelium-section"
|
||||
import { UndernetSection } from "@/components/undernet-section"
|
||||
import { AnastomosisSection } from "@/components/anastomosis-section"
|
||||
import { EmergenceSection } from "@/components/emergence-section"
|
||||
import { NetworkMapSection } from "@/components/network-map-section"
|
||||
import { Footer } from "@/components/footer"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<MycelialCanvas />
|
||||
<main className="relative z-10">
|
||||
<HeroSection />
|
||||
<CompostSection />
|
||||
<MyceliumSection />
|
||||
<UndernetSection />
|
||||
<AnastomosisSection />
|
||||
<EmergenceSection />
|
||||
<NetworkMapSection />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useSectionReveal } from "@/hooks/use-section-reveal"
|
||||
|
||||
export function AnastomosisSection() {
|
||||
const sectionRef = useSectionReveal()
|
||||
const svgRef = useRef<SVGSVGElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const svg = svgRef.current
|
||||
if (!svg) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
svg.querySelectorAll(".draw-line").forEach((el) => {
|
||||
el.classList.add("animate")
|
||||
})
|
||||
svg.querySelector(".merge-dot")?.classList.add("merge-pulse")
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
)
|
||||
|
||||
observer.observe(svg)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative py-32 px-6 noise-overlay">
|
||||
<div className="mycelial-divider mb-20" />
|
||||
|
||||
<div className="max-w-4xl mx-auto space-y-16">
|
||||
<div className="section-reveal space-y-6 text-center">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl md:text-6xl font-bold">
|
||||
Anastomosis
|
||||
</h2>
|
||||
<p className="font-mono text-sm opacity-40">
|
||||
/uh-nas-tuh-MOH-sis/
|
||||
</p>
|
||||
<p className="text-lg sm:text-xl opacity-70 max-w-2xl mx-auto leading-relaxed">
|
||||
When separate mycelial networks discover each other and merge,
|
||||
forming new connections. The moment distinct systems recognize their
|
||||
shared purpose and become one.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* SVG Animation: Two networks merging */}
|
||||
<div className="section-reveal">
|
||||
<svg
|
||||
ref={svgRef}
|
||||
viewBox="0 0 800 300"
|
||||
className="w-full max-w-3xl mx-auto"
|
||||
fill="none"
|
||||
>
|
||||
{/* Left network */}
|
||||
<g>
|
||||
{/* Main trunk */}
|
||||
<path
|
||||
d="M60,150 Q120,130 180,145 Q240,155 290,140"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "0s" }}
|
||||
/>
|
||||
{/* Branch up */}
|
||||
<path
|
||||
d="M140,138 Q160,100 200,85 Q230,75 260,90"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "0.3s" }}
|
||||
/>
|
||||
{/* Branch down */}
|
||||
<path
|
||||
d="M180,145 Q200,180 240,195 Q270,205 300,190"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "0.5s" }}
|
||||
/>
|
||||
{/* Sub-branch */}
|
||||
<path
|
||||
d="M200,85 Q220,60 250,55"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "0.7s" }}
|
||||
/>
|
||||
{/* Small nodes */}
|
||||
<circle cx="60" cy="150" r="3" fill="var(--scroll-accent)" opacity="0.5" />
|
||||
<circle cx="180" cy="145" r="2.5" fill="var(--scroll-accent)" opacity="0.4" />
|
||||
<circle cx="200" cy="85" r="2" fill="var(--scroll-accent)" opacity="0.4" />
|
||||
<circle cx="240" cy="195" r="2" fill="var(--scroll-accent)" opacity="0.4" />
|
||||
</g>
|
||||
|
||||
{/* Right network */}
|
||||
<g>
|
||||
{/* Main trunk */}
|
||||
<path
|
||||
d="M740,150 Q680,135 620,148 Q560,155 510,142"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "0.2s" }}
|
||||
/>
|
||||
{/* Branch up */}
|
||||
<path
|
||||
d="M660,140 Q640,105 600,90 Q570,78 540,92"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "0.4s" }}
|
||||
/>
|
||||
{/* Branch down */}
|
||||
<path
|
||||
d="M620,148 Q600,182 560,198 Q530,208 500,192"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "0.6s" }}
|
||||
/>
|
||||
{/* Sub-branch */}
|
||||
<path
|
||||
d="M600,90 Q580,62 550,58"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "0.8s" }}
|
||||
/>
|
||||
{/* Small nodes */}
|
||||
<circle cx="740" cy="150" r="3" fill="var(--scroll-accent)" opacity="0.5" />
|
||||
<circle cx="620" cy="148" r="2.5" fill="var(--scroll-accent)" opacity="0.4" />
|
||||
<circle cx="600" cy="90" r="2" fill="var(--scroll-accent)" opacity="0.4" />
|
||||
<circle cx="560" cy="198" r="2" fill="var(--scroll-accent)" opacity="0.4" />
|
||||
</g>
|
||||
|
||||
{/* Merge connections (appear last) */}
|
||||
<g>
|
||||
<path
|
||||
d="M290,140 Q340,145 400,150"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "1.8s" }}
|
||||
/>
|
||||
<path
|
||||
d="M510,142 Q460,146 400,150"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: "1.8s" }}
|
||||
/>
|
||||
<path
|
||||
d="M260,90 Q330,95 400,120"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
opacity="0.5"
|
||||
style={{ animationDelay: "2.2s" }}
|
||||
/>
|
||||
<path
|
||||
d="M540,92 Q470,95 400,120"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
opacity="0.5"
|
||||
style={{ animationDelay: "2.2s" }}
|
||||
/>
|
||||
<path
|
||||
d="M300,190 Q350,185 400,175"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
opacity="0.5"
|
||||
style={{ animationDelay: "2.4s" }}
|
||||
/>
|
||||
<path
|
||||
d="M500,192 Q450,185 400,175"
|
||||
className="draw-line"
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
opacity="0.5"
|
||||
style={{ animationDelay: "2.4s" }}
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* Center merge point */}
|
||||
<circle
|
||||
cx="400"
|
||||
cy="150"
|
||||
r="0"
|
||||
className="merge-dot"
|
||||
fill="var(--scroll-accent)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="section-reveal space-y-6 text-center max-w-2xl mx-auto">
|
||||
<p className="text-lg leading-relaxed opacity-75">
|
||||
We are the connections between movements. Commons Stack, MycoFi, the
|
||||
Undernet — separate networks finding each other, merging, growing
|
||||
stronger together. The boundaries between projects dissolve. What
|
||||
remains is the shared mycelium.
|
||||
</p>
|
||||
<p className="text-base opacity-50">
|
||||
A space that belongs to its communities, not its platforms. Find{" "}
|
||||
<a
|
||||
href="https://yourspace.online"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="domain-link"
|
||||
>
|
||||
(you)rSpace.online
|
||||
</a>{" "}
|
||||
and start anastomosing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
"use client"
|
||||
|
||||
import { useSectionReveal } from "@/hooks/use-section-reveal"
|
||||
|
||||
const CARDS = [
|
||||
{
|
||||
title: "Break Down",
|
||||
body: "Old systems don't disappear — they decompose. Capitalism's waste becomes the substrate for what grows next. Every collapsing institution releases nutrients back into the commons.",
|
||||
},
|
||||
{
|
||||
title: "Transform",
|
||||
body: "Mycelium turns death into life. We turn extractive protocols into regenerative ones. The same energy that powered exploitation can power mutual aid — if we know how to compost it.",
|
||||
},
|
||||
{
|
||||
title: "Nourish",
|
||||
body: "What's composted feeds what's growing. Every broken system contains the nutrients for its successor. The question isn't whether the old world will decompose — it's what we grow in its place.",
|
||||
},
|
||||
]
|
||||
|
||||
export function CompostSection() {
|
||||
const sectionRef = useSectionReveal()
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative py-32 px-6 noise-overlay">
|
||||
<div className="mycelial-divider mb-20" />
|
||||
|
||||
<div className="max-w-5xl mx-auto space-y-16">
|
||||
<div className="section-reveal space-y-4 text-center">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl md:text-6xl font-bold">
|
||||
The Compost Layer
|
||||
</h2>
|
||||
<p className="text-lg sm:text-xl opacity-70 max-w-2xl mx-auto">
|
||||
Decomposing extractive systems into nutrients for regeneration
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{CARDS.map((card, i) => (
|
||||
<div
|
||||
key={card.title}
|
||||
className="section-reveal glass-card p-6 space-y-3"
|
||||
style={{ transitionDelay: `${i * 0.15}s` }}
|
||||
>
|
||||
<div
|
||||
className="w-full h-0.5 mb-4"
|
||||
style={{ background: "var(--compost-amber)" }}
|
||||
/>
|
||||
<h3 className="font-serif text-xl font-semibold">
|
||||
{card.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed opacity-75">{card.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="section-reveal text-center text-base opacity-60 max-w-xl mx-auto">
|
||||
This is{" "}
|
||||
<a
|
||||
href="https://compostcapitalism.xyz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="domain-link"
|
||||
>
|
||||
compost capitalism
|
||||
</a>{" "}
|
||||
— the art of breaking down what no longer serves, so that what comes
|
||||
next can thrive.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
"use client"
|
||||
|
||||
import { useSectionReveal } from "@/hooks/use-section-reveal"
|
||||
|
||||
const CARDS = [
|
||||
{
|
||||
title: "Regenerative Economics",
|
||||
body: "New currencies that decompose when hoarded. Mutual credit that flows like nutrients through soil. Quadratic funding that amplifies the grassroots. Economics that serves life instead of extracting from it.",
|
||||
},
|
||||
{
|
||||
title: "Sovereign Technology",
|
||||
body: "Community-owned servers. Open protocols. Software that serves its users. Hardware you can repair. Networks no corporation can capture. The tools of liberation, maintained by the communities that depend on them.",
|
||||
},
|
||||
{
|
||||
title: "Living Commons",
|
||||
body: "Knowledge, tools, and infrastructure that belong to everyone. Not static archives but living, growing resources — tended by communities, enriched by participation, and freely shared across the mycelial web.",
|
||||
},
|
||||
]
|
||||
|
||||
export function EmergenceSection() {
|
||||
const sectionRef = useSectionReveal()
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative py-32 px-6 noise-overlay">
|
||||
<div className="mycelial-divider mb-20" />
|
||||
|
||||
<div className="max-w-5xl mx-auto space-y-16">
|
||||
<div className="section-reveal space-y-6 text-center">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl md:text-6xl font-bold">
|
||||
Emergence
|
||||
</h2>
|
||||
<p className="text-lg sm:text-xl opacity-70 max-w-2xl mx-auto">
|
||||
What grows underground eventually breaks the surface.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quote block */}
|
||||
<blockquote className="section-reveal max-w-2xl mx-auto text-center">
|
||||
<p className="font-serif text-xl sm:text-2xl italic leading-relaxed opacity-80">
|
||||
“The post-capitalist future is not a utopian fantasy. It is
|
||||
already growing, underground, in the networks we are building
|
||||
today.”
|
||||
</p>
|
||||
</blockquote>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{CARDS.map((card, i) => (
|
||||
<div
|
||||
key={card.title}
|
||||
className="section-reveal glass-card p-6 space-y-3"
|
||||
style={{ transitionDelay: `${i * 0.15}s` }}
|
||||
>
|
||||
<div
|
||||
className="w-full h-0.5 mb-4"
|
||||
style={{ background: "var(--emergence-gold)" }}
|
||||
/>
|
||||
<h3 className="font-serif text-xl font-semibold">
|
||||
{card.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed opacity-75">{card.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="section-reveal text-center space-y-4 max-w-xl mx-auto">
|
||||
<p className="text-base opacity-60">
|
||||
Building the{" "}
|
||||
<a
|
||||
href="https://post-appitalist.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="domain-link"
|
||||
>
|
||||
post-appitalist.app
|
||||
</a>
|
||||
lication layer for a regenerative economy. Tools that serve
|
||||
communities, not shareholders.
|
||||
</p>
|
||||
<p className="text-sm opacity-40">
|
||||
Sometimes the best way to see the future is to change your
|
||||
perspective. Stop{" "}
|
||||
<a
|
||||
href="https://trippinballs.lol"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="domain-link"
|
||||
>
|
||||
trippinballs.lol
|
||||
</a>{" "}
|
||||
and start building.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
"use client"
|
||||
|
||||
const LINKS = [
|
||||
{ name: "MycoFi", url: "https://mycofi.earth" },
|
||||
{ name: "Mycopunk", url: "https://mycopunk.xyz" },
|
||||
{ name: "Undernet", url: "https://undernet.earth" },
|
||||
{ name: "Psilo-Cyber", url: "https://psilo-cyber.net" },
|
||||
{ name: "Compost Capitalism", url: "https://compostcapitalism.xyz" },
|
||||
{ name: "Post-Appitalism", url: "https://post-appitalist.app" },
|
||||
{ name: "(You)rSpace", url: "https://yourspace.online" },
|
||||
{ name: "Trippin Balls", url: "https://trippinballs.lol" },
|
||||
]
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="relative z-10 py-20 px-6 border-t border-current/10">
|
||||
<div className="max-w-4xl mx-auto space-y-10">
|
||||
{/* Network links */}
|
||||
<div className="flex flex-wrap justify-center gap-x-6 gap-y-2">
|
||||
{LINKS.map((link) => (
|
||||
<a
|
||||
key={link.url}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-mono text-xs opacity-40 hover:opacity-70 transition-opacity"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tagline */}
|
||||
<div className="text-center space-y-3">
|
||||
<p className="font-serif text-lg opacity-50">
|
||||
Built with compost and code.
|
||||
</p>
|
||||
<p className="font-mono text-xs opacity-30">
|
||||
MycoStack — technology-augmented commons growing from beneath
|
||||
</p>
|
||||
<p className="font-mono text-xs opacity-20">
|
||||
<a
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:opacity-50 transition-opacity"
|
||||
>
|
||||
CC BY-SA 4.0
|
||||
</a>
|
||||
{" "}—{" "}
|
||||
the more we share, the more we have
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
"use client"
|
||||
|
||||
import { useSectionReveal } from "@/hooks/use-section-reveal"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
const TITLE = "MycoStack"
|
||||
|
||||
export function HeroSection() {
|
||||
const sectionRef = useSectionReveal()
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
className="relative min-h-[110vh] flex flex-col items-center justify-center px-6 noise-overlay"
|
||||
>
|
||||
<div className="section-reveal max-w-4xl mx-auto text-center space-y-8">
|
||||
{/* Title with staggered emergence */}
|
||||
<h1 className="font-serif text-6xl sm:text-7xl md:text-8xl lg:text-9xl font-bold tracking-tight">
|
||||
{TITLE.split("").map((letter, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="emerge-letter inline-block"
|
||||
style={{ animationDelay: `${0.3 + i * 0.08}s` }}
|
||||
>
|
||||
{letter}
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p
|
||||
className="emerge-letter text-xl sm:text-2xl md:text-3xl font-serif font-light tracking-wide opacity-80"
|
||||
style={{ animationDelay: "1.2s" }}
|
||||
>
|
||||
Technology-augmented commons.
|
||||
<br />
|
||||
Growing from beneath.
|
||||
</p>
|
||||
|
||||
{/* Terminal tagline */}
|
||||
<div
|
||||
className="emerge-letter font-mono text-sm sm:text-base opacity-60"
|
||||
style={{ animationDelay: "1.8s" }}
|
||||
>
|
||||
<span className="opacity-50">></span> composting capitalism,
|
||||
growing alternatives
|
||||
<span className="cursor-blink ml-0.5">_</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 scroll-hint">
|
||||
<ChevronDown className="w-6 h-6 opacity-40" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
interface Hypha {
|
||||
x: number
|
||||
y: number
|
||||
angle: number
|
||||
speed: number
|
||||
age: number
|
||||
maxAge: number
|
||||
parentX: number
|
||||
parentY: number
|
||||
depth: number
|
||||
branchCount: number
|
||||
}
|
||||
|
||||
export function MycelialCanvas() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const hyphaeRef = useRef<Hypha[]>([])
|
||||
const frameRef = useRef<number>(0)
|
||||
const trailCanvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return
|
||||
|
||||
// Create offscreen trail canvas for persistence
|
||||
const trailCanvas = document.createElement("canvas")
|
||||
const trailCtx = trailCanvas.getContext("2d")!
|
||||
trailCanvasRef.current = trailCanvas
|
||||
|
||||
const resize = () => {
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
||||
const w = window.innerWidth
|
||||
const h = window.innerHeight
|
||||
canvas.width = w * dpr
|
||||
canvas.height = h * dpr
|
||||
canvas.style.width = `${w}px`
|
||||
canvas.style.height = `${h}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
trailCanvas.width = w * dpr
|
||||
trailCanvas.height = h * dpr
|
||||
trailCtx.scale(dpr, dpr)
|
||||
}
|
||||
resize()
|
||||
window.addEventListener("resize", resize)
|
||||
|
||||
const w = () => canvas.width / (Math.min(window.devicePixelRatio || 1, 2))
|
||||
const h = () => canvas.height / (Math.min(window.devicePixelRatio || 1, 2))
|
||||
|
||||
const createHypha = (
|
||||
x: number,
|
||||
y: number,
|
||||
angle: number,
|
||||
depth: number
|
||||
): Hypha => ({
|
||||
x,
|
||||
y,
|
||||
angle,
|
||||
speed: 1.2 + Math.random() * 0.8 - depth * 0.15,
|
||||
age: 0,
|
||||
maxAge: 200 + Math.random() * 150 - depth * 20,
|
||||
parentX: x,
|
||||
parentY: y,
|
||||
depth,
|
||||
branchCount: 0,
|
||||
})
|
||||
|
||||
// Seed initial hyphae from bottom
|
||||
const seedCount = Math.max(3, Math.floor(w() / 250))
|
||||
for (let i = 0; i < seedCount; i++) {
|
||||
const x =
|
||||
(w() / (seedCount + 1)) * (i + 1) + (Math.random() - 0.5) * 80
|
||||
hyphaeRef.current.push(
|
||||
createHypha(
|
||||
x,
|
||||
h() + 10,
|
||||
-Math.PI / 2 + (Math.random() - 0.5) * 0.5,
|
||||
0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const getAccentColor = () => {
|
||||
return (
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--scroll-accent")
|
||||
.trim() || "oklch(0.55 0.18 155)"
|
||||
)
|
||||
}
|
||||
|
||||
let lastSeed = 0
|
||||
|
||||
const animate = () => {
|
||||
const width = w()
|
||||
const height = h()
|
||||
|
||||
// Clear main canvas
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// Fade the trail canvas slowly
|
||||
trailCtx.fillStyle = "rgba(10, 8, 5, 0.008)"
|
||||
trailCtx.fillRect(0, 0, width, height)
|
||||
|
||||
const accent = getAccentColor()
|
||||
const alive: Hypha[] = []
|
||||
|
||||
for (const hypha of hyphaeRef.current) {
|
||||
hypha.age++
|
||||
if (hypha.age >= hypha.maxAge) continue
|
||||
|
||||
// Save previous position
|
||||
const prevX = hypha.x
|
||||
const prevY = hypha.y
|
||||
|
||||
// Random walk with upward bias
|
||||
hypha.angle += (Math.random() - 0.5) * 0.12
|
||||
// Gentle gravitropism (slightly toward vertical)
|
||||
hypha.angle += (-Math.PI / 2 - hypha.angle) * 0.003
|
||||
hypha.x += Math.cos(hypha.angle) * hypha.speed
|
||||
hypha.y += Math.sin(hypha.angle) * hypha.speed
|
||||
|
||||
// Boundary wrapping
|
||||
if (hypha.x < -20) hypha.x = width + 20
|
||||
if (hypha.x > width + 20) hypha.x = -20
|
||||
|
||||
// Opacity based on age and depth
|
||||
const ageRatio = hypha.age / hypha.maxAge
|
||||
const opacity = Math.max(
|
||||
0.05,
|
||||
(1 - ageRatio * 0.9) * (0.6 - hypha.depth * 0.08)
|
||||
)
|
||||
const lineWidth = Math.max(0.3, 2.5 - hypha.depth * 0.4 - ageRatio)
|
||||
|
||||
// Draw on trail canvas for persistence
|
||||
trailCtx.strokeStyle = accent.replace(")", ` / ${opacity * 0.5})`)
|
||||
trailCtx.lineWidth = lineWidth
|
||||
trailCtx.lineCap = "round"
|
||||
trailCtx.beginPath()
|
||||
trailCtx.moveTo(prevX, prevY)
|
||||
trailCtx.lineTo(hypha.x, hypha.y)
|
||||
trailCtx.stroke()
|
||||
|
||||
// Draw tip glow on main canvas
|
||||
if (ageRatio < 0.7) {
|
||||
ctx.fillStyle = accent.replace(")", ` / ${opacity * 0.4})`)
|
||||
ctx.beginPath()
|
||||
ctx.arc(hypha.x, hypha.y, lineWidth + 1, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
// Branching
|
||||
if (
|
||||
hypha.age > 25 &&
|
||||
hypha.age < hypha.maxAge * 0.7 &&
|
||||
hypha.branchCount < 3 &&
|
||||
hypha.depth < 5 &&
|
||||
Math.random() > 0.975
|
||||
) {
|
||||
const branchAngle =
|
||||
hypha.angle + (Math.random() > 0.5 ? 1 : -1) * (0.4 + Math.random() * 0.5)
|
||||
alive.push(createHypha(hypha.x, hypha.y, branchAngle, hypha.depth + 1))
|
||||
hypha.branchCount++
|
||||
}
|
||||
|
||||
alive.push(hypha)
|
||||
}
|
||||
|
||||
// Draw trail canvas behind
|
||||
ctx.drawImage(trailCanvas, 0, 0, width, height)
|
||||
|
||||
// Cap and manage
|
||||
hyphaeRef.current = alive.length > 500 ? alive.slice(-400) : alive
|
||||
|
||||
// Periodically seed new roots
|
||||
const now = Date.now()
|
||||
if (now - lastSeed > 3000 && alive.length < 350) {
|
||||
lastSeed = now
|
||||
const x = Math.random() * width
|
||||
hyphaeRef.current.push(
|
||||
createHypha(x, height + 10, -Math.PI / 2 + (Math.random() - 0.5) * 0.6, 0)
|
||||
)
|
||||
}
|
||||
|
||||
frameRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
animate()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resize)
|
||||
cancelAnimationFrame(frameRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="fixed inset-0 pointer-events-none"
|
||||
style={{ zIndex: 0, opacity: 0.55 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
"use client"
|
||||
|
||||
import { useSectionReveal } from "@/hooks/use-section-reveal"
|
||||
|
||||
const PRINCIPLES = [
|
||||
{
|
||||
title: "Nutrient Cycling",
|
||||
body: "Resources flow where they're needed, not where they're hoarded. Mycelial currencies route value like fungi route nutrients — sensing scarcity, bridging gaps, feeding the weak to strengthen the whole. This is the economics of the forest floor.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 48 48" className="w-12 h-12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="24" cy="24" r="4" />
|
||||
<path d="M24 8v8M24 32v8M8 24h8M32 24h8" />
|
||||
<path d="M14 14l5 5M29 29l5 5M14 34l5-5M29 19l5-5" />
|
||||
<circle cx="24" cy="8" r="2" />
|
||||
<circle cx="24" cy="40" r="2" />
|
||||
<circle cx="8" cy="24" r="2" />
|
||||
<circle cx="40" cy="24" r="2" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Mutual Aid",
|
||||
body: "Every node strengthens the network. Every network strengthens each node. In a mycelial system, there are no freeloaders and no extractors — only participants in a web of reciprocal support. This is the peer-for-peer protocol.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 48 48" className="w-12 h-12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="16" cy="16" r="6" />
|
||||
<circle cx="32" cy="16" r="6" />
|
||||
<circle cx="24" cy="32" r="6" />
|
||||
<path d="M20 20l4 6M28 20l-4 6" />
|
||||
<path d="M16 22v4M32 22v4" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Distributed Intelligence",
|
||||
body: "No central brain. No single point of failure. Intelligence emerges from connection, from the ten thousand chemical conversations happening simultaneously across the network. The wisdom is in the web, not the node.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 48 48" className="w-12 h-12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="24" cy="12" r="3" />
|
||||
<circle cx="12" cy="24" r="3" />
|
||||
<circle cx="36" cy="24" r="3" />
|
||||
<circle cx="18" cy="36" r="3" />
|
||||
<circle cx="30" cy="36" r="3" />
|
||||
<path d="M24 15v6M15 24h6M33 24h-6M20 34l4-10M28 34l-4-10M15 26l3 8M33 26l-3 8" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export function MyceliumSection() {
|
||||
const sectionRef = useSectionReveal()
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative py-32 px-6 noise-overlay">
|
||||
<div className="mycelial-divider mb-20" />
|
||||
|
||||
<div className="max-w-4xl mx-auto space-y-20">
|
||||
<div className="section-reveal space-y-4 text-center">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl md:text-6xl font-bold">
|
||||
The Mycelial Network
|
||||
</h2>
|
||||
<p className="text-lg sm:text-xl opacity-70 max-w-2xl mx-auto">
|
||||
Three principles from the forest floor, applied to human systems
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{PRINCIPLES.map((principle, i) => (
|
||||
<div
|
||||
key={principle.title}
|
||||
className="section-reveal flex gap-8 items-start"
|
||||
style={{ transitionDelay: `${i * 0.12}s` }}
|
||||
>
|
||||
<div className="shrink-0 opacity-50 hidden sm:block mt-1">
|
||||
{principle.icon}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-serif text-2xl sm:text-3xl font-semibold">
|
||||
{principle.title}
|
||||
</h3>
|
||||
<p className="text-base leading-relaxed opacity-75">
|
||||
{principle.body}
|
||||
</p>
|
||||
</div>
|
||||
{i < PRINCIPLES.length - 1 && (
|
||||
<div className="hidden" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Connecting line between principles - visual only */}
|
||||
<div className="section-reveal text-center space-y-3 pt-4">
|
||||
<p className="text-sm opacity-50 max-w-lg mx-auto">
|
||||
These are the protocols of{" "}
|
||||
<a
|
||||
href="https://mycofi.earth"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="domain-link"
|
||||
>
|
||||
mycofi.earth
|
||||
</a>
|
||||
. The economics of interconnection, first practiced by fungi four
|
||||
hundred million years before capitalism. Read more about the
|
||||
philosophy at{" "}
|
||||
<a
|
||||
href="https://mycopunk.xyz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="domain-link"
|
||||
>
|
||||
mycopunk.xyz
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useSectionReveal } from "@/hooks/use-section-reveal"
|
||||
import { ExternalLink } from "lucide-react"
|
||||
|
||||
interface NetworkNode {
|
||||
name: string
|
||||
domain: string
|
||||
description: string
|
||||
x: number
|
||||
y: number
|
||||
primary?: boolean
|
||||
}
|
||||
|
||||
const NODES: NetworkNode[] = [
|
||||
{
|
||||
name: "MycoStack",
|
||||
domain: "mycostack.xyz",
|
||||
description: "Technology-augmented commons",
|
||||
x: 50,
|
||||
y: 45,
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
name: "MycoFi",
|
||||
domain: "mycofi.earth",
|
||||
description: "Mycoeconomics & regenerative currencies",
|
||||
x: 25,
|
||||
y: 20,
|
||||
},
|
||||
{
|
||||
name: "Mycopunk",
|
||||
domain: "mycopunk.xyz",
|
||||
description: "Building from beneath the surface",
|
||||
x: 75,
|
||||
y: 15,
|
||||
},
|
||||
{
|
||||
name: "Compost Capitalism",
|
||||
domain: "compostcapitalism.xyz",
|
||||
description: "Decomposing extractive systems",
|
||||
x: 12,
|
||||
y: 50,
|
||||
},
|
||||
{
|
||||
name: "The Undernet",
|
||||
domain: "undernet.earth",
|
||||
description: "Community-owned infrastructure",
|
||||
x: 88,
|
||||
y: 45,
|
||||
},
|
||||
{
|
||||
name: "Post-Appitalism",
|
||||
domain: "post-appitalist.app",
|
||||
description: "Tools beyond extractive platforms",
|
||||
x: 22,
|
||||
y: 78,
|
||||
},
|
||||
{
|
||||
name: "(You)rSpace",
|
||||
domain: "yourspace.online",
|
||||
description: "Spaces that belong to communities",
|
||||
x: 60,
|
||||
y: 80,
|
||||
},
|
||||
{
|
||||
name: "Psilo-Cyber",
|
||||
domain: "psilo-cyber.net",
|
||||
description: "Encrypted mesh networks",
|
||||
x: 82,
|
||||
y: 72,
|
||||
},
|
||||
{
|
||||
name: "Trippin Balls",
|
||||
domain: "trippinballs.lol",
|
||||
description: "Expand your perspective",
|
||||
x: 42,
|
||||
y: 88,
|
||||
},
|
||||
]
|
||||
|
||||
const CONNECTIONS: [string, string][] = [
|
||||
["mycostack.xyz", "mycofi.earth"],
|
||||
["mycostack.xyz", "undernet.earth"],
|
||||
["mycostack.xyz", "compostcapitalism.xyz"],
|
||||
["mycostack.xyz", "yourspace.online"],
|
||||
["mycofi.earth", "mycopunk.xyz"],
|
||||
["undernet.earth", "psilo-cyber.net"],
|
||||
["compostcapitalism.xyz", "post-appitalist.app"],
|
||||
["post-appitalist.app", "yourspace.online"],
|
||||
["yourspace.online", "trippinballs.lol"],
|
||||
["mycopunk.xyz", "psilo-cyber.net"],
|
||||
["mycofi.earth", "post-appitalist.app"],
|
||||
["mycopunk.xyz", "undernet.earth"],
|
||||
]
|
||||
|
||||
function getNode(domain: string) {
|
||||
return NODES.find((n) => n.domain === domain)
|
||||
}
|
||||
|
||||
export function NetworkMapSection() {
|
||||
const sectionRef = useSectionReveal()
|
||||
const [hovered, setHovered] = useState<string | null>(null)
|
||||
|
||||
const isConnected = (domain: string) => {
|
||||
if (!hovered) return false
|
||||
return CONNECTIONS.some(
|
||||
([a, b]) =>
|
||||
(a === hovered && b === domain) || (b === hovered && a === domain)
|
||||
)
|
||||
}
|
||||
|
||||
const getLineOpacity = (a: string, b: string) => {
|
||||
if (!hovered) return 0.15
|
||||
if (
|
||||
(a === hovered || b === hovered) &&
|
||||
(isConnected(a) || isConnected(b) || a === hovered || b === hovered)
|
||||
)
|
||||
return 0.5
|
||||
return 0.06
|
||||
}
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative py-32 px-6 noise-overlay">
|
||||
<div className="mycelial-divider mb-20" />
|
||||
|
||||
<div className="max-w-5xl mx-auto space-y-16">
|
||||
<div className="section-reveal space-y-4 text-center">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl md:text-6xl font-bold">
|
||||
The Network of Networks
|
||||
</h2>
|
||||
<p className="text-lg sm:text-xl opacity-70 max-w-2xl mx-auto">
|
||||
Every node strengthens the whole. Every connection multiplies
|
||||
possibility.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Desktop: SVG network map */}
|
||||
<div className="section-reveal hidden md:block">
|
||||
<div className="relative" style={{ paddingBottom: "60%" }}>
|
||||
<svg
|
||||
viewBox="0 0 1000 600"
|
||||
className="absolute inset-0 w-full h-full"
|
||||
fill="none"
|
||||
>
|
||||
{/* Connection lines */}
|
||||
{CONNECTIONS.map(([a, b]) => {
|
||||
const nodeA = getNode(a)
|
||||
const nodeB = getNode(b)
|
||||
if (!nodeA || !nodeB) return null
|
||||
return (
|
||||
<line
|
||||
key={`${a}-${b}`}
|
||||
x1={nodeA.x * 10}
|
||||
y1={nodeA.y * 6}
|
||||
x2={nodeB.x * 10}
|
||||
y2={nodeB.y * 6}
|
||||
stroke="var(--scroll-accent)"
|
||||
strokeWidth="1"
|
||||
opacity={getLineOpacity(a, b)}
|
||||
style={{ transition: "opacity 0.3s ease" }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Node cards */}
|
||||
{NODES.map((node) => {
|
||||
const isActive = hovered === node.domain
|
||||
const connected = isConnected(node.domain)
|
||||
const dimmed = hovered && !isActive && !connected
|
||||
|
||||
return (
|
||||
<a
|
||||
key={node.domain}
|
||||
href={`https://${node.domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="network-node absolute -translate-x-1/2 -translate-y-1/2"
|
||||
style={{
|
||||
left: `${node.x}%`,
|
||||
top: `${node.y}%`,
|
||||
opacity: dimmed ? 0.3 : 1,
|
||||
transition: "opacity 0.3s ease, transform 0.3s ease, filter 0.3s ease",
|
||||
}}
|
||||
onMouseEnter={() => setHovered(node.domain)}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
>
|
||||
<div
|
||||
className={`glass-card px-4 py-3 text-center space-y-1 ${
|
||||
node.primary ? "ring-1 ring-current/20" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="font-serif text-sm font-semibold whitespace-nowrap">
|
||||
{node.name}
|
||||
</div>
|
||||
<div className="font-mono text-xs opacity-50 whitespace-nowrap flex items-center gap-1 justify-center">
|
||||
{node.domain}
|
||||
<ExternalLink className="w-2.5 h-2.5" />
|
||||
</div>
|
||||
{isActive && (
|
||||
<div className="text-xs opacity-60 max-w-[160px]">
|
||||
{node.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: Simple card list */}
|
||||
<div className="section-reveal md:hidden grid gap-3 grid-cols-1 sm:grid-cols-2">
|
||||
{NODES.map((node, i) => (
|
||||
<a
|
||||
key={node.domain}
|
||||
href={`https://${node.domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="glass-card p-4 space-y-1 hover:opacity-80 transition-opacity"
|
||||
style={{ transitionDelay: `${i * 0.05}s` }}
|
||||
>
|
||||
<div className="font-serif text-base font-semibold flex items-center gap-2">
|
||||
{node.name}
|
||||
<ExternalLink className="w-3 h-3 opacity-40" />
|
||||
</div>
|
||||
<div className="font-mono text-xs opacity-40">{node.domain}</div>
|
||||
<div className="text-xs opacity-60">{node.description}</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
"use client"
|
||||
|
||||
import { useScrollProgress } from "@/hooks/use-scroll-progress"
|
||||
import { useEffect } from "react"
|
||||
|
||||
// Color stops: [scrollPos, bg-L, bg-C, bg-H, fg-L, fg-C, fg-H, accent-L, accent-C, accent-H]
|
||||
const COLOR_STOPS: number[][] = [
|
||||
[0.0, 0.08, 0.02, 30, 0.85, 0.03, 80, 0.45, 0.12, 60],
|
||||
[0.15, 0.12, 0.04, 40, 0.82, 0.04, 80, 0.55, 0.15, 45],
|
||||
[0.3, 0.16, 0.05, 150, 0.88, 0.05, 145, 0.55, 0.18, 155],
|
||||
[0.5, 0.22, 0.04, 200, 0.9, 0.03, 200, 0.5, 0.14, 220],
|
||||
[0.65, 0.45, 0.06, 140, 0.15, 0.03, 150, 0.65, 0.18, 145],
|
||||
[0.8, 0.85, 0.04, 110, 0.15, 0.04, 100, 0.6, 0.16, 90],
|
||||
[0.92, 0.94, 0.02, 110, 0.12, 0.03, 100, 0.55, 0.18, 155],
|
||||
]
|
||||
|
||||
function lerp(a: number, b: number, t: number) {
|
||||
return a + (b - a) * t
|
||||
}
|
||||
|
||||
function lerpHue(a: number, b: number, t: number) {
|
||||
let diff = b - a
|
||||
if (diff > 180) diff -= 360
|
||||
if (diff < -180) diff += 360
|
||||
return ((a + diff * t) % 360 + 360) % 360
|
||||
}
|
||||
|
||||
function interpolateColors(progress: number) {
|
||||
let i = 0
|
||||
for (; i < COLOR_STOPS.length - 1; i++) {
|
||||
if (progress <= COLOR_STOPS[i + 1][0]) break
|
||||
}
|
||||
const stop = COLOR_STOPS[i]
|
||||
const next = COLOR_STOPS[Math.min(i + 1, COLOR_STOPS.length - 1)]
|
||||
const range = next[0] - stop[0]
|
||||
const t = range > 0 ? Math.min(1, Math.max(0, (progress - stop[0]) / range)) : 0
|
||||
|
||||
return {
|
||||
bg: `oklch(${lerp(stop[1], next[1], t).toFixed(3)} ${lerp(stop[2], next[2], t).toFixed(3)} ${lerpHue(stop[3], next[3], t).toFixed(1)})`,
|
||||
fg: `oklch(${lerp(stop[4], next[4], t).toFixed(3)} ${lerp(stop[5], next[5], t).toFixed(3)} ${lerpHue(stop[6], next[6], t).toFixed(1)})`,
|
||||
accent: `oklch(${lerp(stop[7], next[7], t).toFixed(3)} ${lerp(stop[8], next[8], t).toFixed(3)} ${lerpHue(stop[9], next[9], t).toFixed(1)})`,
|
||||
}
|
||||
}
|
||||
|
||||
export function ScrollProvider({ children }: { children: React.ReactNode }) {
|
||||
const progress = useScrollProgress()
|
||||
|
||||
useEffect(() => {
|
||||
const { bg, fg, accent } = interpolateColors(progress)
|
||||
const root = document.documentElement
|
||||
root.style.setProperty("--scroll-bg", bg)
|
||||
root.style.setProperty("--scroll-fg", fg)
|
||||
root.style.setProperty("--scroll-accent", accent)
|
||||
root.style.setProperty("--scroll-depth", String(progress))
|
||||
}, [progress])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
"use client"
|
||||
|
||||
import { useSectionReveal } from "@/hooks/use-section-reveal"
|
||||
|
||||
const ASCII_NETWORK = ` [node-01] ──── [node-02]
|
||||
│ │
|
||||
│ ┌─────────┘
|
||||
│ │
|
||||
[node-03] ──── [node-04]
|
||||
│ │
|
||||
└──────┬───────┘
|
||||
│
|
||||
[mesh-05]
|
||||
│
|
||||
┌──────┴───────┐
|
||||
│ │
|
||||
[node-06] ──── [node-07]`
|
||||
|
||||
export function UndernetSection() {
|
||||
const sectionRef = useSectionReveal()
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative py-32 px-6 noise-overlay">
|
||||
<div className="mycelial-divider mb-20" />
|
||||
|
||||
<div className="max-w-5xl mx-auto space-y-16">
|
||||
<div className="section-reveal space-y-4 text-center">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl md:text-6xl font-bold">
|
||||
The Undernet
|
||||
</h2>
|
||||
<div className="font-mono text-sm opacity-50">
|
||||
<span className="opacity-40">></span> connecting nodes...
|
||||
building resilience...
|
||||
<span className="cursor-blink ml-0.5">_</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-12 lg:grid-cols-2 items-start">
|
||||
{/* Description */}
|
||||
<div className="section-reveal space-y-6">
|
||||
<h3 className="font-serif text-2xl font-semibold">
|
||||
Community-Owned Infrastructure
|
||||
</h3>
|
||||
<div className="space-y-4 text-base leading-relaxed opacity-75">
|
||||
<p>
|
||||
Beneath the extractive platforms, a different kind of
|
||||
infrastructure is growing. Self-provisioned. Privacy-first.
|
||||
Data sovereign. Locally resilient.
|
||||
</p>
|
||||
<p>
|
||||
Community servers. Encrypted mesh networks. Open protocols that
|
||||
no corporation can shut down. Hardware owned by the people who
|
||||
depend on it. Software that serves its users instead of
|
||||
surveilling them.
|
||||
</p>
|
||||
<p>
|
||||
This is the{" "}
|
||||
<a
|
||||
href="https://undernet.earth"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="domain-link"
|
||||
>
|
||||
undernet.earth
|
||||
</a>{" "}
|
||||
— the network beneath the network. Where the{" "}
|
||||
<a
|
||||
href="https://psilo-cyber.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="domain-link"
|
||||
>
|
||||
psilo-cyber.net
|
||||
</a>
|
||||
work grows, encrypted and entangled, through the substrate of
|
||||
the old world.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-4 space-y-2 font-mono text-xs opacity-60">
|
||||
<div>
|
||||
<span className="opacity-50">protocol:</span> fog computing
|
||||
</div>
|
||||
<div>
|
||||
<span className="opacity-50">governance:</span> mycological
|
||||
consensus
|
||||
</div>
|
||||
<div>
|
||||
<span className="opacity-50">ownership:</span> community
|
||||
</div>
|
||||
<div>
|
||||
<span className="opacity-50">surveillance:</span>{" "}
|
||||
<span style={{ color: "var(--mycelium-green)" }}>none</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ASCII Network Diagram */}
|
||||
<div
|
||||
className="section-reveal glass-card p-6 overflow-x-auto"
|
||||
style={{ transitionDelay: "0.2s" }}
|
||||
>
|
||||
<div className="font-mono text-xs sm:text-sm leading-relaxed opacity-60 whitespace-pre">
|
||||
{ASCII_NETWORK}
|
||||
</div>
|
||||
<p className="mt-4 font-mono text-xs opacity-40">
|
||||
// every node is sovereign
|
||||
<br />
|
||||
// every connection is encrypted
|
||||
<br />// the network has no center
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
services:
|
||||
mycostack-website:
|
||||
build: .
|
||||
container_name: mycostack-website
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
- DAC_OVERRIDE
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/cache/nginx
|
||||
- /var/run
|
||||
networks:
|
||||
- traefik-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.mycostack.rule=Host(`mycostack.xyz`) || Host(`www.mycostack.xyz`)"
|
||||
- "traefik.http.routers.mycostack.entrypoints=web"
|
||||
- "traefik.http.services.mycostack.loadbalancer.server.port=80"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
|
||||
export function useScrollProgress() {
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
const scrollTop = window.scrollY
|
||||
const docHeight =
|
||||
document.documentElement.scrollHeight - window.innerHeight
|
||||
const scrollPercent = docHeight > 0 ? scrollTop / docHeight : 0
|
||||
setProgress(Math.min(1, Math.max(0, scrollPercent)))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let ticking = false
|
||||
const onScroll = () => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
handleScroll()
|
||||
ticking = false
|
||||
})
|
||||
ticking = true
|
||||
}
|
||||
}
|
||||
window.addEventListener("scroll", onScroll, { passive: true })
|
||||
handleScroll()
|
||||
return () => window.removeEventListener("scroll", onScroll)
|
||||
}, [handleScroll])
|
||||
|
||||
return progress
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
export function useSectionReveal() {
|
||||
const ref = useRef<HTMLElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("visible")
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.15, rootMargin: "0px 0px -50px 0px" }
|
||||
)
|
||||
|
||||
const revealElements = el.querySelectorAll(".section-reveal")
|
||||
revealElements.forEach((child) => observer.observe(child))
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
return ref
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
eslint: { ignoreDuringBuilds: true },
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
images: { unoptimized: true },
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
||||
location /_next/static/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
||||
gzip_min_length 1000;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "mycostack-website",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"geist": "^1.3.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "^15.3.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwind-merge": "^2.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
"@types/node": "^22.15.0",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"postcss": "^8.5.4",
|
||||
"tailwindcss": "^4.1.9",
|
||||
"tw-animate-css": "^1.3.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Reference in New Issue