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:
Musa 2025-12-18 15:55:15 -08:00 committed by GitHub
parent 48bbc7cce7
commit 0c3efdbef2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
119 changed files with 27142 additions and 266 deletions

View 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;
}
}

View 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"
}
}

View 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"
}
}

View 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;

View 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"]
}

View 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"
]
}

View 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
View 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"
}
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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 };

View 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
View 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";

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));
}

14
packages/ui/tsconfig.json Normal file
View 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"]
}