initial commit

This commit is contained in:
2026-03-15 22:47:27 +01:00
commit afb939f629
39 changed files with 7810 additions and 0 deletions

1
.automancer/context Submodule

Submodule .automancer/context added at 1f0cf387c3

1
.claude Symbolic link
View File

@@ -0,0 +1 @@
.automancer/context/.claude

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# dependencies
/node_modules
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
# env files
.env*.local
# typescript
*.tsbuildinfo
next-env.d.ts

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule ".automancer/context"]
path = .automancer/context
url = ssh://gitea@git.cynarski.pl:65522/automancer/context.git

6
.tasks/build.yml Normal file
View File

@@ -0,0 +1,6 @@
version: "3"
tasks:
default:
desc: "Zbuduj projekt produkcyjnie"
cmd: npx next build

6
.tasks/clean.yml Normal file
View File

@@ -0,0 +1,6 @@
version: "3"
tasks:
default:
desc: "Wyczyść artefakty budowania"
cmd: rm -rf .next out

10
.tasks/dev.yml Normal file
View File

@@ -0,0 +1,10 @@
version: "3"
tasks:
run:
desc: "Uruchom serwer deweloperski"
cmd: npx next dev --turbopack
watch:
desc: "Uruchom serwer deweloperski z hot-reload (alias dla run)"
cmd: npx next dev --turbopack

12
.tasks/lint.yml Normal file
View File

@@ -0,0 +1,12 @@
version: "3"
tasks:
check:
desc: "Sprawdź kod (ESLint + TypeScript)"
cmds:
- npx next lint
- npx tsc --noEmit
fix:
desc: "Napraw problemy z lintingiem"
cmd: npx next lint --fix

10
.tasks/test.yml Normal file
View File

@@ -0,0 +1,10 @@
version: "3"
tasks:
unit:
desc: "Uruchom testy jednostkowe"
cmd: echo "No tests configured yet"
all:
desc: "Uruchom wszystkie testy"
cmd: echo "No tests configured yet"

24
CLAUDE.md Normal file
View File

@@ -0,0 +1,24 @@
<!-- extends: .automancer/context/CLAUDE.md -->
# Automancer Site
Strona projektu Automancer.
## Kontekst projektu
Wspólny kontekst (architektura, konwencje, słownik, workflow) znajduje się w submodule:
`.automancer/context/` — patrz `.automancer/context/CLAUDE.md`
## Design System
Pełna dokumentacja design systemu:
- Wytyczne: `.automancer/context/design-system/guidelines.md`
- Tokeny: `.automancer/context/design-system/tokens.json`
- Logo (SVG): `.automancer/context/design-system/logo/`
- Mockupy: `.automancer/context/assets/mockups/`
## Stack
- Dark mode jako domyślny motyw
- Paleta: burgundy primary (`#7A3B4E`), terracotta accent (`#C2703E`), warm stone neutrals
- Typografia: Inter (UI), JetBrains Mono (kod)

11
Taskfile.yml Normal file
View File

@@ -0,0 +1,11 @@
version: "3"
vars:
PROJECT_NAME: automancer-site
includes:
dev: .tasks/dev.yml
build: .tasks/build.yml
lint: .tasks/lint.yml
test: .tasks/test.yml
clean: .tasks/clean.yml

18
eslint.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

7
next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

6614
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "automancer-site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"clsx": "^2.1.1",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

5
public/favicon.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024">
<g transform="translate(0,1024) scale(0.1,-0.1)" stroke="none">
<path fill="#C07790" fill-rule="evenodd" d="M4285 5939 c-918 -14 -1184 -46 -1420 -168 -89 -46 -199 -150 -249 -235 l-39 -66 -146 0 -147 0 -30 -31 -29 -30 0 -339 0 -340 28 -27 c27 -27 30 -28 170 -33 l142 -5 9 -40 c17 -82 49 -181 71 -225 55 -108 154 -200 290 -268 304 -152 913 -199 1260 -97 174 51 324 154 419 288 24 34 85 144 135 244 101 200 146 258 236 303 42 21 64 25 135 25 72 0 92 -4 135 -27 98 -51 145 -113 240 -314 36 -75 85 -166 109 -202 124 -187 310 -303 559 -349 132 -25 517 -25 677 0 328 51 527 133 659 272 94 99 133 186 167 373 4 22 7 22 145 22 140 0 141 0 168 28 l28 28 7 194 c4 107 5 261 4 342 l-3 147 -29 30 -30 31 -147 0 -148 0 -31 55 c-115 202 -324 315 -670 365 -207 29 -426 40 -1035 50 -618 10 -939 10 -1640 -1z M7040 4965 l0 -515 -510 0 -510 0 0 515 0 515 510 0 510 0 0 -515z M6160 4971 l0 -370 363 -3 c199 -2 365 -2 370 0 4 2 7 169 7 373 l0 369 -370 0 -370 0 0 -369z M3585 5360 c150 -98 466 -303 543 -351 92 -57 88 -64 -113 -191 -93 -60 -251 -160 -350 -222 -99 -63 -188 -118 -198 -122 -9 -3 -24 -1 -32 6 -13 11 -15 78 -15 474 0 465 2 487 35 486 5 -1 64 -36 130 -80z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,15 @@
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen">
{/* Sidebar — placeholder for future implementation */}
<aside className="hidden w-64 border-r border-surface-border bg-surface-alt lg:block">
<div className="p-6 text-sm text-text-muted">Dashboard sidebar</div>
</aside>
<main className="flex-1 p-6">{children}</main>
</div>
);
}

View File

@@ -0,0 +1,11 @@
export default function OverviewPage() {
return (
<div className="mx-auto max-w-4xl">
<h1 className="mb-4 text-2xl font-bold text-neutral-50">Overview</h1>
<p className="text-text-secondary">
Dashboard w budowie. Tu pojawi się monitoring agentów, pipeline&apos;ów
i wizualizacje.
</p>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import { Background } from "@/components/layout/background";
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Background />
<Header />
<main className="relative z-10">{children}</main>
<Footer className="relative z-10" />
</>
);
}

View File

@@ -0,0 +1,17 @@
import { Hero } from "@/components/landing/hero";
import { Architecture } from "@/components/landing/architecture";
import { Features } from "@/components/landing/features";
import { TechStack } from "@/components/landing/tech-stack";
import { CTA } from "@/components/landing/cta";
export default function LandingPage() {
return (
<>
<Hero />
<Architecture />
<Features />
<TechStack />
<CTA />
</>
);
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

97
src/app/globals.css Normal file
View File

@@ -0,0 +1,97 @@
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&family=Oxanium:wght@200..800&display=swap');
@import "tailwindcss";
@theme inline {
/* Primary — Burgundy */
--color-primary-50: #F9F0F3;
--color-primary-100: #F0D9E0;
--color-primary-200: #DBA8B8;
--color-primary-300: #C07790;
--color-primary-400: #9C4E6A;
--color-primary-500: #7A3B4E;
--color-primary-600: #632F40;
--color-primary-700: #4D2332;
--color-primary-800: #381824;
--color-primary-900: #230E17;
/* Accent — Terracotta */
--color-accent-50: #FFF7ED;
--color-accent-100: #FFEDD5;
--color-accent-200: #FED7AA;
--color-accent-300: #FDBA74;
--color-accent-400: #FB923C;
--color-accent-500: #C2703E;
--color-accent-600: #A35A2F;
--color-accent-700: #7C4422;
--color-accent-800: #5C3318;
--color-accent-900: #3D210F;
/* Neutral — Warm Stone */
--color-neutral-50: #FAFAF9;
--color-neutral-100: #F5F5F4;
--color-neutral-200: #E7E5E4;
--color-neutral-300: #D6D3D1;
--color-neutral-400: #A8A29E;
--color-neutral-500: #78716C;
--color-neutral-600: #57534E;
--color-neutral-700: #44403C;
--color-neutral-800: #292524;
--color-neutral-900: #1C1917;
/* Semantic */
--color-success: #4A7A5B;
--color-success-light: #D1FAE5;
--color-success-dark: #2D4F38;
--color-warning: #A38A2F;
--color-warning-light: #FEF3C7;
--color-warning-dark: #6B5A1E;
--color-error: #9B3B3B;
--color-error-light: #FEE2E2;
--color-error-dark: #6B2525;
--color-info: #4A6B82;
--color-info-light: #DBEAFE;
--color-info-dark: #2E4454;
/* Surface */
--color-surface-bg: #1C1917;
--color-surface-alt: #292524;
--color-surface-card: #292524;
--color-surface-card-hover: #44403C;
--color-surface-border: #44403C;
--color-surface-border-subtle: #292524;
/* Text */
--color-text-primary: #FAFAF9;
--color-text-secondary: #A8A29E;
--color-text-muted: #78716C;
--color-text-inverse: #1C1917;
--color-text-accent: #DBA8B8;
--color-text-link: #C07790;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(28, 25, 23, 0.3);
--shadow-md: 0 4px 6px rgba(28, 25, 23, 0.4);
--shadow-lg: 0 10px 15px rgba(28, 25, 23, 0.5);
--shadow-glow: 0 0 20px rgba(122, 59, 78, 0.3);
/* Typography */
--font-sans: "Oxanium", system-ui, -apple-system, sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
--font-display: "Orbitron", system-ui, sans-serif;
/* Border radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
}
body {
background: var(--color-surface-bg);
color: var(--color-text-primary);
font-family: var(--font-sans);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
}

36
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import "./globals.css";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin", "latin-ext"],
});
const jetbrainsMono = JetBrains_Mono({
variable: "--font-jetbrains-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Automancer — Agentic AI Platform",
description:
"Platforma agentic AI oparta o Kubernetes, OpenSandbox i Claude. Orkiestracja agentów, pipeline'y CI/CD, izolowane środowiska.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="pl" className="dark">
<body
className={`${inter.variable} ${jetbrainsMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,81 @@
import { cn } from "@/lib/utils";
type LogoVariant = "dark" | "light" | "colored";
type LogoSize = "sm" | "md" | "lg" | "xl" | "2xl";
interface LogoProps {
variant?: LogoVariant;
size?: LogoSize;
showWordmark?: boolean;
className?: string;
}
const sizeMap: Record<LogoSize, number> = {
sm: 32,
md: 48,
lg: 64,
xl: 96,
"2xl": 160,
};
const fillMap: Record<LogoVariant, string> = {
dark: "#C07790",
light: "#7A3B4E",
colored: "#C07790",
};
export function Logo({ variant = "dark", size = "md", showWordmark = false, className }: LogoProps) {
const px = sizeMap[size];
const fill = fillMap[variant];
return (
<div className={cn("inline-flex items-center", className)}>
<svg
width={px}
height={px}
viewBox="0 0 1024 1024"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="Automancer logo"
>
<g transform="translate(0,1024) scale(0.1,-0.1)" stroke="none">
<path
fill={fill}
d="M4890 8184 c-529 -65 -843 -162 -1405 -434 -241 -117 -427 -195 -568 -240 -73 -23 -141 -46 -152 -50 -16 -6 -5 -15 65 -49 47 -22 123 -50 170 -62 47 -12 91 -23 99 -25 8 -3 -28 -47 -92 -114 -229 -239 -389 -467 -448 -639 -22 -64 -22 -61 -6 -57 6 3 24 7 40 10 l27 6 0 -130 c0 -72 5 -174 11 -228 9 -86 53 -337 64 -370 3 -8 30 5 80 37 42 28 112 64 155 81 68 25 243 65 380 85 35 5 36 7 64 93 39 118 112 290 168 393 74 133 57 124 173 100 492 -100 962 -99 1455 5 152 31 373 95 693 198 363 117 378 121 388 105 5 -8 9 -19 9 -25 0 -6 9 -39 21 -74 70 -213 224 -449 445 -679 l103 -108 108 -13 c237 -28 439 -95 557 -185 52 -40 48 -44 71 68 76 379 51 778 -64 1032 -129 284 -364 461 -656 491 l-60 7 -42 86 c-166 346 -566 577 -1158 668 -96 14 -611 27 -695 17z"
/>
<path
fill={fill}
fillRule="evenodd"
d="M4285 5939 c-918 -14 -1184 -46 -1420 -168 -89 -46 -199 -150 -249 -235 l-39 -66 -146 0 -147 0 -30 -31 -29 -30 0 -339 0 -340 28 -27 c27 -27 30 -28 170 -33 l142 -5 9 -40 c17 -82 49 -181 71 -225 55 -108 154 -200 290 -268 304 -152 913 -199 1260 -97 174 51 324 154 419 288 24 34 85 144 135 244 101 200 146 258 236 303 42 21 64 25 135 25 72 0 92 -4 135 -27 98 -51 145 -113 240 -314 36 -75 85 -166 109 -202 124 -187 310 -303 559 -349 132 -25 517 -25 677 0 328 51 527 133 659 272 94 99 133 186 167 373 4 22 7 22 145 22 140 0 141 0 168 28 l28 28 7 194 c4 107 5 261 4 342 l-3 147 -29 30 -30 31 -147 0 -148 0 -31 55 c-115 202 -324 315 -670 365 -207 29 -426 40 -1035 50 -618 10 -939 10 -1640 -1z M7040 4965 l0 -515 -510 0 -510 0 0 515 0 515 510 0 510 0 0 -515z M6160 4971 l0 -370 363 -3 c199 -2 365 -2 370 0 4 2 7 169 7 373 l0 369 -370 0 -370 0 0 -369z M3585 5360 c150 -98 466 -303 543 -351 92 -57 88 -64 -113 -191 -93 -60 -251 -160 -350 -222 -99 -63 -188 -118 -198 -122 -9 -3 -24 -1 -32 6 -13 11 -15 78 -15 474 0 465 2 487 35 486 5 -1 64 -36 130 -80z"
/>
<path
fill={fill}
d="M7120 3959 c-30 -10 -64 -18 -75 -19 -36 -1 -45 -16 -70 -116 -102 -421 -306 -788 -600 -1079 -266 -263 -587 -441 -935 -517 -130 -29 -477 -32 -616 -5 -591 113 -1118 542 -1399 1140 -59 125 -127 319 -160 453 l-27 110 -85 23 c-46 13 -86 22 -88 19 -7 -7 52 -243 92 -363 114 -348 281 -644 501 -888 322 -358 728 -586 1177 -663 123 -21 423 -24 540 -5 825 131 1490 768 1744 1670 40 139 65 251 59 255 -1 1 -28 -5 -58 -15z"
/>
</g>
</svg>
{showWordmark && (
<span
className={cn(
"font-display font-bold uppercase tracking-wider",
size === "sm" && "-ml-1",
size === "md" && "-ml-2",
size === "lg" && "-ml-2",
size === "xl" && "-ml-3",
size === "2xl" && "-ml-5",
size === "sm" && "text-base",
size === "md" && "text-xl",
size === "lg" && "text-2xl",
size === "xl" && "text-4xl",
size === "2xl" && "text-6xl",
variant === "dark" && "text-primary-300",
variant === "light" && "text-primary-500",
variant === "colored" && "text-primary-300"
)}
>
Automancer
</span>
)}
</div>
);
}

View File

@@ -0,0 +1,46 @@
export function Architecture() {
const nodes = [
{ label: "User", color: "bg-neutral-700 text-neutral-50", col: "col-start-2" },
{ label: "Pipeline CI/CD", color: "bg-success text-neutral-50", col: "col-start-2" },
{ label: "Agent Runtime", color: "bg-primary-500 text-neutral-50", col: "col-start-1" },
{ label: "Kubernetes", color: "bg-info text-neutral-50", col: "col-start-2" },
{ label: "Claude AI", color: "bg-primary-700 text-primary-200", col: "col-start-3" },
{ label: "OpenSandbox", color: "bg-accent-500 text-neutral-50", col: "col-start-1" },
{ label: "Containers", color: "bg-accent-600 text-neutral-50", col: "col-start-3" },
];
return (
<section id="architecture" className="px-6 py-24">
<div className="mx-auto max-w-6xl">
<h2 className="mb-4 text-center text-3xl font-bold text-neutral-50 md:text-4xl">
Architektura
</h2>
<p className="mx-auto mb-16 max-w-2xl text-center text-text-secondary">
Przepływ od użytkownika przez pipeline, agenty AI do izolowanych
kontenerów na Kubernetes.
</p>
<div className="mx-auto max-w-md">
<div className="grid grid-cols-3 gap-4">
{nodes.map((node) => (
<div
key={node.label}
className={`${node.color} ${node.col} flex items-center justify-center rounded-[var(--radius-md)] px-4 py-3 text-sm font-medium shadow-[var(--shadow-md)]`}
>
{node.label}
</div>
))}
</div>
{/* Connection lines via border decorations */}
<div className="mt-8 flex justify-center">
<div className="flex items-center gap-2 text-xs text-text-muted">
<span className="inline-block h-px w-8 bg-primary-300" />
<span>Active flow</span>
<span className="ml-4 inline-block h-px w-8 bg-neutral-600" />
<span>Connection</span>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,22 @@
import { Button } from "@/components/ui/button";
export function CTA() {
return (
<section className="px-6 py-24">
<div className="mx-auto max-w-2xl text-center">
<h2 className="mb-4 text-3xl font-bold text-neutral-50 md:text-4xl">
Gotowy na start?
</h2>
<p className="mb-8 text-text-secondary">
Zacznij orkiestrować agentów AI już dziś.
</p>
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
<Button size="lg">Rozpocznij</Button>
<Button variant="ghost" size="lg">
Zobacz dokumentację
</Button>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,72 @@
import { Card } from "@/components/ui/card";
const features = [
{
title: "Agent Runtime",
description:
"Uruchamiaj agentów AI w izolowanych środowiskach z pełną kontrolą nad cyklem życia i zasobami.",
icon: (
<svg className="h-8 w-8 text-primary-300" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456Z" />
</svg>
),
},
{
title: "Pipeline Orchestration",
description:
"Definiuj pipeline'y CI/CD do budowania, testowania i wdrażania agentów z pełnym observability.",
icon: (
<svg className="h-8 w-8 text-success" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" />
</svg>
),
},
{
title: "Kubernetes Native",
description:
"Natywna integracja z Kubernetes — auto-scaling, health checks, rolling updates dla agentów.",
icon: (
<svg className="h-8 w-8 text-info" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
</svg>
),
},
{
title: "Claude-Powered",
description:
"Claude jako silnik agentów — rozumowanie, analiza kodu, podejmowanie decyzji w kontekście pipeline'u.",
icon: (
<svg className="h-8 w-8 text-accent-400" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" />
</svg>
),
},
];
export function Features() {
return (
<section id="features" className="px-6 py-24">
<div className="mx-auto max-w-6xl">
<h2 className="mb-4 text-center text-3xl font-bold text-neutral-50 md:text-4xl">
Funkcje
</h2>
<p className="mx-auto mb-16 max-w-2xl text-center text-text-secondary">
Wszystko czego potrzebujesz do orkiestracji agentów AI w produkcji.
</p>
<div className="grid gap-6 sm:grid-cols-2">
{features.map((feature) => (
<Card key={feature.title} hoverable>
<div className="mb-4">{feature.icon}</div>
<h3 className="mb-2 text-xl font-semibold text-neutral-50">
{feature.title}
</h3>
<p className="text-text-secondary leading-relaxed">
{feature.description}
</p>
</Card>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,22 @@
import { Logo } from "@/components/brand/logo";
import { Button } from "@/components/ui/button";
export function Hero() {
return (
<section className="relative flex min-h-[80vh] flex-col items-center justify-center px-6 py-24 text-center">
<div className="relative z-10 flex flex-col items-center gap-8">
<Logo variant="colored" size="2xl" showWordmark />
<p className="max-w-2xl text-lg leading-relaxed text-text-secondary md:text-xl">
Platforma agentic AI orkiestracja agentów, pipeline&apos;y CI/CD
i izolowane środowiska uruchomieniowe oparte o Kubernetes.
</p>
<div className="flex flex-col gap-4 sm:flex-row">
<Button size="lg">Rozpocznij</Button>
<Button variant="secondary" size="lg">
Dokumentacja
</Button>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,32 @@
import { Badge } from "@/components/ui/badge";
const stack = [
{ label: "Kubernetes", variant: "k8s" as const },
{ label: "Containers", variant: "container" as const },
{ label: "GitLab CI/CD", variant: "pipeline" as const },
{ label: "Claude AI", variant: "agent" as const },
{ label: "OpenSandbox", variant: "container" as const },
{ label: "Pipeline Engine", variant: "pipeline" as const },
];
export function TechStack() {
return (
<section id="tech" className="px-6 py-24">
<div className="mx-auto max-w-6xl">
<h2 className="mb-4 text-center text-3xl font-bold text-neutral-50 md:text-4xl">
Tech Stack
</h2>
<p className="mx-auto mb-12 max-w-2xl text-center text-text-secondary">
Sprawdzone technologie połączone w spójną platformę.
</p>
<div className="flex flex-wrap items-center justify-center gap-3">
{stack.map((item) => (
<Badge key={item.label} variant={item.variant}>
{item.label}
</Badge>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,356 @@
"use client";
import { useEffect, useRef, useCallback } from "react";
interface Node {
x: number;
y: number;
baseX: number;
baseY: number;
vx: number;
vy: number;
isHub: boolean;
pulsePhase: number;
radius: number;
}
interface Pulse {
edgeIdx: number;
progress: number;
speed: number;
direction: 1 | -1;
}
const NODE_COUNT = 80;
const MAX_EDGE_DIST = 180;
const MOUSE_RADIUS = 250;
const MOUSE_REPEL = 0.3;
const DRIFT_SPEED = 0.15;
const RETURN_FORCE = 0.005;
function createNodes(w: number, h: number): Node[] {
const nodes: Node[] = [];
for (let i = 0; i < NODE_COUNT; i++) {
const x = Math.random() * w;
const y = Math.random() * h;
nodes.push({
x,
y,
baseX: x,
baseY: y,
vx: (Math.random() - 0.5) * DRIFT_SPEED,
vy: (Math.random() - 0.5) * DRIFT_SPEED,
isHub: i < 12,
pulsePhase: Math.random() * Math.PI * 2,
radius: i < 12 ? 2.5 : 1.2,
});
}
return nodes;
}
function buildEdges(nodes: Node[]): [number, number][] {
const edges: [number, number][] = [];
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const dx = nodes[i].x - nodes[j].x;
const dy = nodes[i].y - nodes[j].y;
if (dx * dx + dy * dy < MAX_EDGE_DIST * MAX_EDGE_DIST) {
edges.push([i, j]);
}
}
}
return edges;
}
export function Background() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const nodesRef = useRef<Node[]>([]);
const pulsesRef = useRef<Pulse[]>([]);
const mouseRef = useRef({ x: -1000, y: -1000 });
const frameRef = useRef<number>(0);
const timeRef = useRef(0);
const spawnPulse = useCallback((edges: [number, number][]) => {
if (edges.length === 0) return;
const idx = Math.floor(Math.random() * edges.length);
pulsesRef.current.push({
edgeIdx: idx,
progress: 0,
speed: 0.008 + Math.random() * 0.012,
direction: Math.random() > 0.5 ? 1 : -1,
});
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let w = window.innerWidth;
let h = window.innerHeight;
canvas.width = w;
canvas.height = h;
nodesRef.current = createNodes(w, h);
pulsesRef.current = [];
const handleResize = () => {
w = window.innerWidth;
h = window.innerHeight;
canvas.width = w;
canvas.height = h;
nodesRef.current = createNodes(w, h);
pulsesRef.current = [];
};
const handleMouseMove = (e: MouseEvent) => {
mouseRef.current = { x: e.clientX, y: e.clientY };
};
const handleMouseLeave = () => {
mouseRef.current = { x: -1000, y: -1000 };
};
window.addEventListener("resize", handleResize);
window.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseleave", handleMouseLeave);
const animate = () => {
timeRef.current += 1;
const nodes = nodesRef.current;
const mx = mouseRef.current.x;
const my = mouseRef.current.y;
ctx.clearRect(0, 0, w, h);
// Update node positions
for (const node of nodes) {
// Drift
node.x += node.vx;
node.y += node.vy;
// Return to base
node.vx += (node.baseX - node.x) * RETURN_FORCE;
node.vy += (node.baseY - node.y) * RETURN_FORCE;
// Damping
node.vx *= 0.99;
node.vy *= 0.99;
// Mouse interaction — repel
const dmx = node.x - mx;
const dmy = node.y - my;
const distMouse = Math.sqrt(dmx * dmx + dmy * dmy);
if (distMouse < MOUSE_RADIUS && distMouse > 0) {
const force = (1 - distMouse / MOUSE_RADIUS) * MOUSE_REPEL;
node.vx += (dmx / distMouse) * force;
node.vy += (dmy / distMouse) * force;
}
// Boundary wrap
if (node.x < -20) node.x = w + 20;
if (node.x > w + 20) node.x = -20;
if (node.y < -20) node.y = h + 20;
if (node.y > h + 20) node.y = -20;
}
// Build edges dynamically
const edges = buildEdges(nodes);
// Draw edges
for (let i = 0; i < edges.length; i++) {
const [a, b] = edges[i];
const dx = nodes[a].x - nodes[b].x;
const dy = nodes[a].y - nodes[b].y;
const dist = Math.sqrt(dx * dx + dy * dy);
const alpha = (1 - dist / MAX_EDGE_DIST) * 0.12;
// Brighter edges near mouse
const edgeMx = (nodes[a].x + nodes[b].x) / 2;
const edgeMy = (nodes[a].y + nodes[b].y) / 2;
const edgeMouseDist = Math.sqrt(
(edgeMx - mx) ** 2 + (edgeMy - my) ** 2
);
const mouseBoost =
edgeMouseDist < MOUSE_RADIUS
? (1 - edgeMouseDist / MOUSE_RADIUS) * 0.2
: 0;
ctx.beginPath();
ctx.moveTo(nodes[a].x, nodes[a].y);
ctx.lineTo(nodes[b].x, nodes[b].y);
ctx.strokeStyle = `rgba(192, 119, 144, ${alpha + mouseBoost})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
// Spawn pulses
if (timeRef.current % 8 === 0) {
spawnPulse(edges);
}
// Draw & update pulses
const activePulses: Pulse[] = [];
for (const pulse of pulsesRef.current) {
pulse.progress += pulse.speed;
if (pulse.progress > 1) {
// Chain reaction — sometimes spawn a new pulse from the destination node
if (Math.random() < 0.4 && pulse.edgeIdx < edges.length) {
const destNode =
pulse.direction === 1
? edges[pulse.edgeIdx][1]
: edges[pulse.edgeIdx][0];
// Find connected edges from destination
const connected = edges
.map((e, idx) => ({ e, idx }))
.filter(
({ e, idx }) =>
idx !== pulse.edgeIdx &&
(e[0] === destNode || e[1] === destNode)
);
if (connected.length > 0) {
const next =
connected[Math.floor(Math.random() * connected.length)];
activePulses.push({
edgeIdx: next.idx,
progress: 0,
speed: 0.01 + Math.random() * 0.01,
direction:
next.e[0] === destNode ? 1 : -1,
});
}
}
continue;
}
if (pulse.edgeIdx >= edges.length) continue;
const [a, b] = edges[pulse.edgeIdx];
const na = nodes[a];
const nb = nodes[b];
const t = pulse.direction === 1 ? pulse.progress : 1 - pulse.progress;
const px = na.x + (nb.x - na.x) * t;
const py = na.y + (nb.y - na.y) * t;
// Pulse head glow
const gradient = ctx.createRadialGradient(px, py, 0, px, py, 12);
gradient.addColorStop(0, "rgba(192, 119, 144, 0.6)");
gradient.addColorStop(0.5, "rgba(192, 119, 144, 0.15)");
gradient.addColorStop(1, "rgba(192, 119, 144, 0)");
ctx.beginPath();
ctx.arc(px, py, 12, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
// Bright core
ctx.beginPath();
ctx.arc(px, py, 2, 0, Math.PI * 2);
ctx.fillStyle = "rgba(219, 168, 184, 0.8)";
ctx.fill();
// Trail
const trailLen = 0.15;
const tStart = Math.max(0, t - trailLen);
const sx = na.x + (nb.x - na.x) * tStart;
const sy = na.y + (nb.y - na.y) * tStart;
const trailGrad = ctx.createLinearGradient(sx, sy, px, py);
trailGrad.addColorStop(0, "rgba(192, 119, 144, 0)");
trailGrad.addColorStop(1, "rgba(192, 119, 144, 0.3)");
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(px, py);
ctx.strokeStyle = trailGrad;
ctx.lineWidth = 1.5;
ctx.stroke();
activePulses.push(pulse);
}
pulsesRef.current = activePulses;
// Draw nodes
for (const node of nodes) {
const pulse =
Math.sin(timeRef.current * 0.03 + node.pulsePhase) * 0.5 + 0.5;
// Mouse proximity glow
const dMouse = Math.sqrt(
(node.x - mx) ** 2 + (node.y - my) ** 2
);
const mouseGlow =
dMouse < MOUSE_RADIUS ? (1 - dMouse / MOUSE_RADIUS) : 0;
if (node.isHub) {
// Hub outer glow
const glowR = node.radius * (3 + pulse * 2) + mouseGlow * 8;
const glowAlpha = 0.06 + pulse * 0.04 + mouseGlow * 0.1;
const gradient = ctx.createRadialGradient(
node.x, node.y, 0,
node.x, node.y, glowR
);
gradient.addColorStop(0, `rgba(192, 119, 144, ${glowAlpha})`);
gradient.addColorStop(1, "rgba(192, 119, 144, 0)");
ctx.beginPath();
ctx.arc(node.x, node.y, glowR, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
// Hub core
ctx.beginPath();
ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(192, 119, 144, ${0.4 + pulse * 0.2 + mouseGlow * 0.3})`;
ctx.fill();
} else {
ctx.beginPath();
ctx.arc(node.x, node.y, node.radius + mouseGlow * 1.5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(120, 113, 108, ${0.15 + mouseGlow * 0.4})`;
ctx.fill();
}
}
frameRef.current = requestAnimationFrame(animate);
};
frameRef.current = requestAnimationFrame(animate);
return () => {
cancelAnimationFrame(frameRef.current);
window.removeEventListener("resize", handleResize);
window.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseleave", handleMouseLeave);
};
}, [spawnPulse]);
return (
<div className="pointer-events-none fixed inset-0 z-0 overflow-hidden">
<canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />
{/* Primary glow — top center */}
<div
className="absolute -top-[20%] left-1/2 h-[800px] w-[800px] -translate-x-1/2 rounded-full"
style={{
background:
"radial-gradient(circle, rgba(122,59,78,0.15) 0%, rgba(122,59,78,0.05) 40%, transparent 70%)",
}}
/>
{/* Accent glow — bottom right */}
<div
className="absolute -bottom-[10%] -right-[10%] h-[600px] w-[600px] rounded-full"
style={{
background:
"radial-gradient(circle, rgba(194,112,62,0.08) 0%, rgba(194,112,62,0.03) 40%, transparent 70%)",
}}
/>
{/* Secondary glow — left */}
<div
className="absolute top-[40%] -left-[10%] h-[500px] w-[500px] rounded-full"
style={{
background:
"radial-gradient(circle, rgba(74,107,130,0.06) 0%, transparent 60%)",
}}
/>
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { cn } from "@/lib/utils";
import { Logo } from "@/components/brand/logo";
export function Footer({ className }: { className?: string }) {
return (
<footer className={cn("border-t border-surface-border bg-surface-bg/80 py-12", className)}>
<div className="mx-auto max-w-6xl px-6">
<div className="flex flex-col items-center gap-6 md:flex-row md:justify-between">
<Logo variant="dark" size="sm" showWordmark />
<div className="flex items-center gap-6 text-sm text-text-muted">
<a href="#" className="hover:text-text-primary transition-colors">Dokumentacja</a>
<a href="https://gitlab.com/automancer" className="hover:text-text-primary transition-colors">GitLab</a>
<a href="#" className="hover:text-text-primary transition-colors">Kontakt</a>
</div>
</div>
<div className="mt-8 text-center text-xs text-text-muted">
&copy; 2026 Automancer. Agentic AI Platform.
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,24 @@
import { Logo } from "@/components/brand/logo";
import { Button } from "@/components/ui/button";
export function Header() {
return (
<header className="sticky top-0 z-50 w-full border-b border-surface-border bg-surface-bg/80 backdrop-blur-md">
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
<Logo variant="dark" size="sm" showWordmark />
<nav className="hidden items-center gap-6 md:flex">
<a href="#features" className="text-sm text-text-secondary hover:text-text-primary transition-colors">
Funkcje
</a>
<a href="#architecture" className="text-sm text-text-secondary hover:text-text-primary transition-colors">
Architektura
</a>
<a href="#tech" className="text-sm text-text-secondary hover:text-text-primary transition-colors">
Stack
</a>
</nav>
<Button size="sm">Rozpocznij</Button>
</div>
</header>
);
}

View File

@@ -0,0 +1,31 @@
import { cn } from "@/lib/utils";
type BadgeVariant = "pipeline" | "k8s" | "container" | "agent" | "default";
interface BadgeProps {
variant?: BadgeVariant;
children: React.ReactNode;
className?: string;
}
const variantStyles: Record<BadgeVariant, string> = {
pipeline: "bg-primary-700 text-primary-200",
k8s: "bg-info-dark text-info-light",
container: "bg-accent-700 text-accent-200",
agent: "bg-primary-500 text-neutral-50",
default: "bg-neutral-700 text-neutral-300",
};
export function Badge({ variant = "default", children, className }: BadgeProps) {
return (
<span
className={cn(
"inline-flex items-center gap-1.5 px-3 py-1 text-sm font-medium rounded-[var(--radius-sm)]",
variantStyles[variant],
className
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,51 @@
import { cn } from "@/lib/utils";
import { type ButtonHTMLAttributes, forwardRef } from "react";
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
type ButtonSize = "sm" | "md" | "lg";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
as?: "button" | "a";
href?: string;
}
const variantStyles: Record<ButtonVariant, string> = {
primary:
"bg-primary-500 text-neutral-50 hover:bg-primary-400 active:bg-primary-600",
secondary:
"bg-transparent text-primary-300 border border-primary-500 hover:bg-primary-500/10 active:bg-primary-500/20",
ghost:
"bg-transparent text-neutral-400 hover:text-neutral-50 hover:bg-neutral-700/50",
danger:
"bg-error text-neutral-50 hover:bg-error/80 active:bg-error-dark",
};
const sizeStyles: Record<ButtonSize, string> = {
sm: "px-3 py-1.5 text-sm rounded-[var(--radius-sm)]",
md: "px-5 py-2.5 text-base rounded-[var(--radius-md)]",
lg: "px-7 py-3 text-lg rounded-[var(--radius-md)]",
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "primary", size = "md", children, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
"inline-flex items-center justify-center font-medium transition-colors cursor-pointer",
variantStyles[variant],
sizeStyles[size],
className
)}
{...props}
>
{children}
</button>
);
}
);
Button.displayName = "Button";
export { Button, type ButtonProps };

View File

@@ -0,0 +1,27 @@
import { cn } from "@/lib/utils";
import { type HTMLAttributes, forwardRef } from "react";
interface CardProps extends HTMLAttributes<HTMLDivElement> {
hoverable?: boolean;
}
const Card = forwardRef<HTMLDivElement, CardProps>(
({ className, hoverable = false, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"bg-surface-card border border-surface-border rounded-[var(--radius-lg)] p-6",
hoverable && "transition-all hover:bg-surface-card-hover hover:shadow-[var(--shadow-glow)]",
className
)}
{...props}
>
{children}
</div>
);
}
);
Card.displayName = "Card";
export { Card, type CardProps };

4
src/lib/tokens.ts Normal file
View File

@@ -0,0 +1,4 @@
import tokens from "../../.automancer/context/design-system/tokens.json";
export type Tokens = typeof tokens;
export default tokens;

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"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": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}