mirror of
https://github.com/katanemo/plano.git
synced 2026-04-25 00:36:34 +02:00
feat: redesign archgw -> plano + website in Next.js (#613)
* feat: redesign archgw -> plano + website * feat(www): refactor landing page sections, add new diagrams and UI improvements * feat(www): sections enhanced for clarify & diagrams added * feat(www): improvements to mobile design, layout of diagrams * feat(www): clean + typecheck * feat(www): feedback loop changes * feat(www): fix type error * fix lib/utils error * feat(www): ran biome formatting * feat(www): graphic changes * feat(www): web analytics * fea(www): changes * feat(www): introduce monorepo This change brings Turborepo monorepo to independently handle the marketing website, the docs website and any other future use cases for mutli-platform support. They are using internal @katanemo package handlers for the design system and logic. * fix(www): transpiler failure * fix(www): tsconfig issue * fix(www): next.config issue * feat(docs): hold off on docs * Delete next.config.ts * feat(www): content fix * feat(www): introduce blog * feat(www): content changes * Update package-lock.json * feat: update text * Update IntroSection.tsx * feat: Turbopack issue * fix * Update IntroSection.tsx * feat: updated Research page * refactor(www): text clarity, padding adj. * format(www) * fix: add missing lib/ files to git - fixes Vercel GitHub deployment - Updated .gitignore to properly exclude Python lib/ but include Next.js lib/ directories - Added packages/ui/src/lib/utils.ts (cn utility function) - Added apps/www/src/lib/sanity.ts (Sanity client configuration) - Fixes module resolution errors in Vercel GitHub deployments (case-sensitive filesystem) * Update .gitignore * style(www): favicon + metadata * fix(www): links * fix(www): add analytics * fix(www): add * fix(www): fix links + image * fix(www): fix links + image * fix(www): fix links * fix(www): remove from tools testing.md
This commit is contained in:
parent
48bbc7cce7
commit
0c3efdbef2
119 changed files with 27142 additions and 266 deletions
230
packages/shared-styles/globals.css
Normal file
230
packages/shared-styles/globals.css
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@source "../../apps/*/src/**/*.{js,ts,jsx,tsx,mdx}";
|
||||
@source "../../packages/ui/src/**/*.{js,ts,jsx,tsx,mdx}";
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* Custom breakpoint for extra large screens */
|
||||
@theme {
|
||||
--breakpoint-3xl: 1920px;
|
||||
}
|
||||
|
||||
/* Font Face Declarations */
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
src: url("/fonts/IBMPlexSans-VariableFont_wdth,wght.ttf")
|
||||
format("truetype-variations");
|
||||
font-weight: 100 700; /* Variable font weight range */
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
src: url("/fonts/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf")
|
||||
format("truetype-variations");
|
||||
font-weight: 100 700; /* Variable font weight range */
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("/fonts/jetbrains-mono-bold.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Base colors */
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
|
||||
/* Brand colors from Figma */
|
||||
--primary: #7780d9;
|
||||
--primary-dark: #4141b2;
|
||||
--primary-darker: #2a3178;
|
||||
--secondary: #9797ea;
|
||||
|
||||
/* Purple shades */
|
||||
--purple-50: #edefff;
|
||||
--purple-100: #b9bfff;
|
||||
--purple-200: #b0b7ff;
|
||||
--purple-300: #abb2fa;
|
||||
--purple-400: #969ff4;
|
||||
--purple-500: #acb3fe;
|
||||
|
||||
/* Neutral colors */
|
||||
--muted: #787878;
|
||||
--muted-foreground: #494949;
|
||||
--border: #d1d1d1;
|
||||
|
||||
/* Dark section */
|
||||
--dark-bg: #1a1a1a;
|
||||
--dark-foreground: #ffffff;
|
||||
--radius: 0.625rem;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #171717;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #171717;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary-foreground: #171717;
|
||||
--accent: #edefff;
|
||||
--accent-foreground: #171717;
|
||||
--destructive: #ef4444;
|
||||
--input: #d1d1d1;
|
||||
--ring: #7780d9;
|
||||
--chart-1: #7780d9;
|
||||
--chart-2: #9797ea;
|
||||
--chart-3: #abb2fa;
|
||||
--chart-4: #969ff4;
|
||||
--chart-5: #acb3fe;
|
||||
--sidebar: #ffffff;
|
||||
--sidebar-foreground: #171717;
|
||||
--sidebar-primary: #7780d9;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: #edefff;
|
||||
--sidebar-accent-foreground: #171717;
|
||||
--sidebar-border: #d1d1d1;
|
||||
--sidebar-ring: #7780d9;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-dark: var(--primary-dark);
|
||||
--color-primary-darker: var(--primary-darker);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-purple-50: var(--purple-50);
|
||||
--color-purple-100: var(--purple-100);
|
||||
--color-purple-200: var(--purple-200);
|
||||
--color-purple-300: var(--purple-300);
|
||||
--color-purple-400: var(--purple-400);
|
||||
--color-purple-500: var(--purple-500);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-dark-bg: var(--dark-bg);
|
||||
--color-dark-foreground: var(--dark-foreground);
|
||||
|
||||
/* Font families for Tailwind */
|
||||
--font-sans: "IBM Plex Sans", system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
|
||||
/* Font weights - variable font supports 100-700 */
|
||||
--font-weight-thin: 100;
|
||||
--font-weight-extralight: 200;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
/* Global letter-spacing removed - use Tailwind tracking utilities (tracking-tight, tracking-[-1.92px], etc.) instead */
|
||||
|
||||
/* Apply custom font to body by default */
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
html {
|
||||
background: linear-gradient(to top right, #ffffff, #dcdfff);
|
||||
min-height: 100%;
|
||||
}
|
||||
body {
|
||||
@apply text-foreground;
|
||||
font-family: var(--font-sans);
|
||||
background: linear-gradient(to top right, #ffffff, #dcdfff);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
17
packages/shared-styles/package.json
Normal file
17
packages/shared-styles/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@katanemo/shared-styles",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "./globals.css",
|
||||
"exports": {
|
||||
"./globals.css": "./globals.css"
|
||||
},
|
||||
"files": [
|
||||
"globals.css"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "echo 'Skipping build'",
|
||||
"lint": "biome check",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
12
packages/tailwind-config/package.json
Normal file
12
packages/tailwind-config/package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "@katanemo/tailwind-config",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"tailwind.config.ts"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4"
|
||||
}
|
||||
}
|
||||
15
packages/tailwind-config/tailwind.config.ts
Normal file
15
packages/tailwind-config/tailwind.config.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"../../apps/*/src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"../../packages/ui/src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
19
packages/tsconfig/base.json
Normal file
19
packages/tsconfig/base.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"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
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
23
packages/tsconfig/nextjs.json
Normal file
23
packages/tsconfig/nextjs.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@katanemo/ui": ["../../packages/ui/src"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
]
|
||||
}
|
||||
9
packages/tsconfig/package.json
Normal file
9
packages/tsconfig/package.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@katanemo/tsconfig",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
"nextjs.json"
|
||||
]
|
||||
}
|
||||
54
packages/ui/package.json
Normal file
54
packages/ui/package.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "@katanemo/ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./lib/utils": {
|
||||
"types": "./src/lib/utils.ts",
|
||||
"default": "./src/lib/utils.ts"
|
||||
},
|
||||
"./components/*": {
|
||||
"types": "./src/components/*.tsx",
|
||||
"default": "./src/components/*.tsx"
|
||||
},
|
||||
"./components/ui/*": {
|
||||
"types": "./src/components/ui/*.tsx",
|
||||
"default": "./src/components/ui/*.tsx"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "echo 'Skipping build'",
|
||||
"lint": "biome check",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^0.548.0",
|
||||
"next": "16.0.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^16.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
}
|
||||
}
|
||||
109
packages/ui/src/components/Footer.tsx
Normal file
109
packages/ui/src/components/Footer.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
const footerLinks = {
|
||||
product: [
|
||||
{ label: "Research", href: "/research" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Documentation", href: "https://docs.planoai.dev", external: true },
|
||||
{ label: "Hugging Face", href: "https://huggingface.co/katanemo", external: true },
|
||||
],
|
||||
resources: [
|
||||
{ label: "GitHub", href: "https://github.com/katanemo/arch", external: true },
|
||||
{ label: "Discord", href: "https://discord.gg/pGZf2gcwEc", external: true },
|
||||
{ label: "Get Started", href: "https://docs.planoai.dev/get_started/installation", external: true },
|
||||
],
|
||||
};
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer
|
||||
className="relative overflow-hidden pt-20 px-6 lg:px-[102px] pb-48"
|
||||
style={{ background: "linear-gradient(to top right, #ffffff, #dcdfff)" }}
|
||||
>
|
||||
<div className="max-w-[81rem] mx-auto relative z-10">
|
||||
{/* Main Grid Layout */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20">
|
||||
{/* Left Column - Tagline and Copyright */}
|
||||
<div className="flex flex-col">
|
||||
<p className="ext-base sm:text-lg md:text-xl lg:text-[22px] font-sans font-normal tracking-[-1.0px] sm:tracking-[-1.22px]! text-black mb-6 sm:mb-8">
|
||||
Plano is a powerful agent delivery infrastructure platform that is framework-friendly,
|
||||
and empowers developers and teams to seamlessly build, deliver, and scale agentic
|
||||
applications.
|
||||
</p>
|
||||
|
||||
{/* Copyright */}
|
||||
<div className="mt-auto">
|
||||
<p className="font-sans text-sm sm:text-base text-black/63 tracking-[-0.6px] sm:tracking-[-0.8px]!">
|
||||
© Katanemo Labs, Inc. 2025 / Plano by Katanemo Labs, Inc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Navigation Links */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8">
|
||||
{/* Product Links */}
|
||||
<div>
|
||||
<h3 className="font-sans font-normal text-xl sm:text-2xl lg:text-3xl text-black tracking-[-1.2px] sm:tracking-[-1.4px] lg:tracking-[-1.6px]! mb-4 sm:mb-6">
|
||||
Product
|
||||
</h3>
|
||||
<nav className="space-y-3 sm:space-y-4">
|
||||
{footerLinks.product.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target={link.external ? "_blank" : undefined}
|
||||
rel={link.external ? "noopener noreferrer" : undefined}
|
||||
className="block font-sans font-normal text-sm sm:text-base lg:text-lg text-black tracking-[-0.8px] sm:tracking-[-0.9px] lg:tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Resources Links */}
|
||||
<div>
|
||||
<h3 className="font-sans font-normal text-xl sm:text-2xl lg:text-3xl text-black tracking-[-1.2px] sm:tracking-[-1.4px] lg:tracking-[-1.6px]! mb-4 sm:mb-6">
|
||||
Resources
|
||||
</h3>
|
||||
<nav className="space-y-3 sm:space-y-4">
|
||||
{footerLinks.resources.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target={link.external ? "_blank" : undefined}
|
||||
rel={link.external ? "noopener noreferrer" : undefined}
|
||||
className="block font-sans font-normal text-sm sm:text-base lg:text-lg text-black tracking-[-0.8px] sm:tracking-[-0.9px] lg:tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Half-Cut Plano Logo Background */}
|
||||
<div className="absolute bottom-0 left-0 right-0 overflow-hidden pointer-events-none">
|
||||
<div className="max-w-[81rem] mx-auto px-6 lg:px-[1px]">
|
||||
<div className="relative w-full flex justify-start">
|
||||
<Image
|
||||
src="/LogoOutline.svg"
|
||||
alt="Plano Logo"
|
||||
width={1800}
|
||||
height={200}
|
||||
className="w-150 h-auto opacity-30 select-none"
|
||||
style={{
|
||||
transform: "translateY(0%)", // Push logo down more while showing top part
|
||||
transformOrigin: "center bottom",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
17
packages/ui/src/components/Logo.tsx
Normal file
17
packages/ui/src/components/Logo.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{/* LogoMarkSquare SVG */}
|
||||
<Image
|
||||
src="/Logomark.svg"
|
||||
alt="Plano Logo"
|
||||
width={90}
|
||||
height={20}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
289
packages/ui/src/components/Navbar.tsx
Normal file
289
packages/ui/src/components/Navbar.tsx
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { Logo } from "./Logo";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "../lib/utils";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { X, Menu } from "lucide-react";
|
||||
|
||||
const navItems = [
|
||||
{ href: "https://docs.planoai.dev/get_started/quickstart", label: "start locally" },
|
||||
{ href: "https://docs.planoai.dev", label: "docs" },
|
||||
{ href: "/research", label: "research" },
|
||||
{ href: "/blog", label: "blog" },
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isDarkBackground, setIsDarkBackground] = useState(false);
|
||||
|
||||
// Detect background color behind dropdown menu
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) {
|
||||
setIsDarkBackground(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const detectBackground = () => {
|
||||
// Small delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
const nav = document.querySelector("nav");
|
||||
if (!nav) return;
|
||||
|
||||
const navRect = nav.getBoundingClientRect();
|
||||
const dropdownBottom = navRect.bottom;
|
||||
const checkY = dropdownBottom + 20; // Just below the dropdown
|
||||
|
||||
// First, try to find section elements directly
|
||||
const main = document.querySelector("main");
|
||||
if (main) {
|
||||
const sections = main.querySelectorAll("section");
|
||||
let foundDarkSection = false;
|
||||
|
||||
sections.forEach((section) => {
|
||||
const rect = section.getBoundingClientRect();
|
||||
// Check if this section is visible below the navbar
|
||||
if (rect.top <= checkY && rect.bottom > checkY) {
|
||||
// Check for dark background classes
|
||||
const classList = Array.from(section.classList);
|
||||
const hasDarkBg = classList.some(
|
||||
(cls) =>
|
||||
cls.includes("bg-[#1a1a1a]") ||
|
||||
cls.includes("bg-black") ||
|
||||
cls.includes("bg-gray-900") ||
|
||||
cls.includes("bg-neutral-900") ||
|
||||
cls.includes("dark"),
|
||||
);
|
||||
|
||||
if (hasDarkBg) {
|
||||
foundDarkSection = true;
|
||||
setIsDarkBackground(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check computed background
|
||||
const computed = window.getComputedStyle(section);
|
||||
const bg = computed.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
const rgbMatch = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const r = parseInt(rgbMatch[1]);
|
||||
const g = parseInt(rgbMatch[2]);
|
||||
const b = parseInt(rgbMatch[3]);
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
setIsDarkBackground(luminance < 0.5);
|
||||
foundDarkSection = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (foundDarkSection) return;
|
||||
}
|
||||
|
||||
// Fallback: Check element at point
|
||||
const centerX = window.innerWidth / 2;
|
||||
const elementBelow = document.elementFromPoint(centerX, checkY);
|
||||
|
||||
if (elementBelow) {
|
||||
let current: HTMLElement | null = elementBelow as HTMLElement;
|
||||
let backgroundColor = "";
|
||||
|
||||
// Walk up the DOM tree
|
||||
let levels = 0;
|
||||
while (
|
||||
current &&
|
||||
!backgroundColor &&
|
||||
current !== document.body &&
|
||||
levels < 15
|
||||
) {
|
||||
const computed = window.getComputedStyle(current);
|
||||
const bg = computed.backgroundColor;
|
||||
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
const rgbaMatch = bg.match(
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
|
||||
);
|
||||
if (rgbaMatch) {
|
||||
const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
|
||||
if (alpha > 0.1) {
|
||||
backgroundColor = bg;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
backgroundColor = bg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
current = current.parentElement;
|
||||
levels++;
|
||||
}
|
||||
|
||||
if (!backgroundColor) {
|
||||
const bodyBg = window.getComputedStyle(
|
||||
document.body,
|
||||
).backgroundColor;
|
||||
backgroundColor = bodyBg;
|
||||
}
|
||||
|
||||
if (backgroundColor) {
|
||||
const rgbMatch = backgroundColor.match(
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+)/,
|
||||
);
|
||||
if (rgbMatch) {
|
||||
const r = parseInt(rgbMatch[1]);
|
||||
const g = parseInt(rgbMatch[2]);
|
||||
const b = parseInt(rgbMatch[3]);
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
setIsDarkBackground(luminance < 0.5);
|
||||
} else {
|
||||
const darkColors = [
|
||||
"black",
|
||||
"#000",
|
||||
"#000000",
|
||||
"rgb(0,0,0)",
|
||||
"rgba(0,0,0",
|
||||
"#1a1a1a",
|
||||
];
|
||||
const isDark = darkColors.some((color) =>
|
||||
backgroundColor.toLowerCase().includes(color.toLowerCase()),
|
||||
);
|
||||
setIsDarkBackground(isDark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Detect on open and on scroll
|
||||
detectBackground();
|
||||
const scrollHandler = () => detectBackground();
|
||||
const resizeHandler = () => detectBackground();
|
||||
|
||||
window.addEventListener("scroll", scrollHandler, { passive: true });
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", scrollHandler);
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
// Close menu when route changes
|
||||
const handleLinkClick = () => {
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="relative z-50 bg-gradient-to-b from-transparent to-white/5 backdrop-blur border-b border-neutral-200/5">
|
||||
<div className="max-w-[85rem] mx-auto px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center">
|
||||
<Logo />
|
||||
</Link>
|
||||
|
||||
{/* Navigation Links and CTA - Far Right */}
|
||||
<div className="hidden md:flex items-center justify-end gap-8">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"text-lg font-medium text-[var(--muted)]",
|
||||
"hover:text-[var(--primary)] transition-colors",
|
||||
"font-mono tracking-tighter",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsMenuOpen(!isMenuOpen);
|
||||
}}
|
||||
className="p-2 rounded-md text-[var(--muted)] hover:text-[var(--primary)] transition-colors"
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={isMenuOpen}
|
||||
>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{isMenuOpen ? (
|
||||
<motion.div
|
||||
key="close"
|
||||
initial={{ opacity: 0, rotate: -90 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="menu"
|
||||
initial={{ opacity: 0, rotate: 90 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: -90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Dropdown Menu - Outside constrained container for full width */}
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="md:hidden overflow-hidden bg-[#7580DF]/70"
|
||||
>
|
||||
<div className="max-w-[85rem] mx-auto px-6 lg:px-8 py-3">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{navItems.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.href}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: index * 0.05,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={handleLinkClick}
|
||||
className={cn(
|
||||
"block px-0 py-1.5 border-b border-dashed transition-colors font-mono tracking-tighter",
|
||||
"text-sm font-medium",
|
||||
"text-white",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
65
packages/ui/src/components/ui/button.tsx
Normal file
65
packages/ui/src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[7px] font-mono font-medium tracking-[-0.989px] transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-[#7780d9] border-[#4141b2] border-[1.562px] border-solid text-white hover:bg-[#7780d9]/90 text-base leading-[1.102]",
|
||||
primary:
|
||||
"bg-[#7780d9] border-[#4141b2] border-[1.562px] border-solid text-white hover:bg-[#7780d9]/90 text-base leading-[1.102]",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-[#edefff] border-[#d1d1d1] border-[1.562px] border-solid text-[#494949] hover:bg-[#edefff]/90 text-base leading-[1.102]",
|
||||
secondaryDark:
|
||||
"bg-neutral-600 border-[#d1d1d1]/20 border-[1.562px] border-solid text-white hover:bg-[#1a1a1a]/90 text-base leading-[1.102]",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-5 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-[7px] px-5 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
143
packages/ui/src/components/ui/dialog.tsx
Normal file
143
packages/ui/src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
22
packages/ui/src/index.ts
Normal file
22
packages/ui/src/index.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Components
|
||||
export { Logo } from "./components/Logo";
|
||||
export { Footer } from "./components/Footer";
|
||||
export { Navbar } from "./components/Navbar";
|
||||
|
||||
// UI Components
|
||||
export { Button, buttonVariants } from "./components/ui/button";
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./components/ui/dialog";
|
||||
|
||||
// Utils
|
||||
export { cn } from "./lib/utils";
|
||||
6
packages/ui/src/lib/utils.ts
Normal file
6
packages/ui/src/lib/utils.ts
Normal 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));
|
||||
}
|
||||
14
packages/ui/tsconfig.json
Normal file
14
packages/ui/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "@katanemo/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue