Initial commit: Korrith website, assessment API, PocketBase schema, Docker/Coolify deployment
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Routes, BrowserRouter as Router, useLocation } from 'react-router-dom';
|
||||
import ScrollToTop from '@/components/ScrollToTop.jsx';
|
||||
import { useGoogleAnalytics } from '@/hooks/useGoogleAnalytics.js';
|
||||
|
||||
// Pages
|
||||
import HomePage from '@/pages/HomePage.jsx';
|
||||
import ArgusGridPage from '@/pages/ArgusGridPage.jsx';
|
||||
import ArgusDetailPage from '@/pages/ArgusDetailPage.jsx';
|
||||
import ExternalPosturePage from '@/pages/ExternalPosturePage.jsx';
|
||||
import InternalPosturePage from '@/pages/InternalPosturePage.jsx';
|
||||
import ActiveValidationPage from '@/pages/ActiveValidationPage.jsx';
|
||||
import ReportsAndEvidencePage from '@/pages/ReportsAndEvidencePage.jsx';
|
||||
import QuantumReadinessPage from '@/pages/QuantumReadinessPage.jsx';
|
||||
import RoadmapPage from '@/pages/RoadmapPage.jsx';
|
||||
import AgenciesPage from '@/pages/AgenciesPage.jsx';
|
||||
import MSSPsPage from '@/pages/MSSPsPage.jsx';
|
||||
import BusinessesPage from '@/pages/BusinessesPage.jsx';
|
||||
import WebsiteSecurityAuditPage from '@/pages/WebsiteSecurityAuditPage.jsx';
|
||||
import ManagedSecureHostingPage from '@/pages/ManagedSecureHostingPage.jsx';
|
||||
import PostQuantumReadinessPage from '@/pages/PostQuantumReadinessPage.jsx';
|
||||
import WhyKorrithPage from '@/pages/WhyKorrithPage.jsx';
|
||||
import BlogPage from '@/pages/BlogPage.jsx';
|
||||
import SampleReportPage from '@/pages/SampleReportPage.jsx';
|
||||
import DocsPage from '@/pages/DocsPage.jsx';
|
||||
import DocumentationPage from '@/pages/DocumentationPage.jsx';
|
||||
import ResearchPage from '@/pages/ResearchPage.jsx';
|
||||
import FAQPage from '@/pages/FAQPage.jsx';
|
||||
import AboutPage from '@/pages/AboutPage.jsx';
|
||||
import PartnersPage from '@/pages/PartnersPage.jsx';
|
||||
import TrustPage from '@/pages/TrustPage.jsx';
|
||||
import ContactPage from '@/pages/ContactPage.jsx';
|
||||
import AssessmentPage from '@/pages/AssessmentPage.jsx';
|
||||
import PrivacyPage from '@/pages/PrivacyPage.jsx';
|
||||
import TermsPage from '@/pages/TermsPage.jsx';
|
||||
import LoginPage from '@/pages/LoginPage.jsx';
|
||||
|
||||
// Analytics Tracker Component
|
||||
function AnalyticsTracker() {
|
||||
const location = useLocation();
|
||||
const { initializeGA, trackPageView } = useGoogleAnalytics();
|
||||
|
||||
useEffect(() => {
|
||||
initializeGA();
|
||||
}, [initializeGA]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`Route changed to: ${location.pathname}`);
|
||||
|
||||
// Small delay to ensure gtag is initialized and document title is updated
|
||||
const timeoutId = setTimeout(() => {
|
||||
trackPageView(location.pathname + location.search, document.title);
|
||||
}, 150);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [location, trackPageView]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<AnalyticsTracker />
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
|
||||
{/* Auth Routes */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
{/* Platform Routes */}
|
||||
<Route path="/platform/argus-grid" element={<ArgusGridPage />} />
|
||||
<Route path="/platform/argus" element={<ArgusDetailPage />} />
|
||||
<Route path="/platform/external-posture" element={<ExternalPosturePage />} />
|
||||
<Route path="/platform/internal-posture" element={<InternalPosturePage />} />
|
||||
<Route path="/platform/active-validation" element={<ActiveValidationPage />} />
|
||||
<Route path="/platform/reports-and-evidence" element={<ReportsAndEvidencePage />} />
|
||||
<Route path="/platform/quantum-readiness" element={<QuantumReadinessPage />} />
|
||||
<Route path="/platform/roadmap" element={<RoadmapPage />} />
|
||||
|
||||
{/* Solutions Routes */}
|
||||
<Route path="/solutions/agencies" element={<AgenciesPage />} />
|
||||
<Route path="/solutions/mssps" element={<MSSPsPage />} />
|
||||
<Route path="/solutions/businesses" element={<BusinessesPage />} />
|
||||
<Route path="/solutions/website-security-audits" element={<WebsiteSecurityAuditPage />} />
|
||||
<Route path="/solutions/managed-secure-hosting" element={<ManagedSecureHostingPage />} />
|
||||
<Route path="/solutions/post-quantum-readiness" element={<PostQuantumReadinessPage />} />
|
||||
|
||||
{/* Other Routes */}
|
||||
<Route path="/why-korrith" element={<WhyKorrithPage />} />
|
||||
<Route path="/blog" element={<BlogPage />} />
|
||||
<Route path="/resources/sample-report" element={<SampleReportPage />} />
|
||||
<Route path="/docs" element={<DocsPage />} />
|
||||
<Route path="/resources/documentation" element={<DocumentationPage />} />
|
||||
<Route path="/research" element={<ResearchPage />} />
|
||||
<Route path="/faq" element={<FAQPage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
<Route path="/partners" element={<PartnersPage />} />
|
||||
<Route path="/trust" element={<TrustPage />} />
|
||||
<Route path="/contact" element={<ContactPage />} />
|
||||
<Route path="/assessment" element={<AssessmentPage />} />
|
||||
<Route path="/privacy" element={<PrivacyPage />} />
|
||||
<Route path="/terms" element={<TermsPage />} />
|
||||
|
||||
{/* Fallback Route */}
|
||||
<Route path="*" element={<HomePage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Github, Twitter, Linkedin } from 'lucide-react';
|
||||
|
||||
export default function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-card border-t border-border pt-16 pb-8 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Top Section: CTA & Newsletter */}
|
||||
<div className="flex flex-col lg:flex-row justify-between items-start gap-12 mb-16 pb-16 border-b border-border/50">
|
||||
<div className="max-w-md">
|
||||
<h2 className="text-2xl font-bold text-card-foreground mb-4">Secure infrastructure. Quantum future.</h2>
|
||||
<p className="text-muted-foreground mb-6">Experience the next generation of security posture management and quantum readiness.</p>
|
||||
<Button asChild size="lg" className="bg-primary text-primary-foreground hover:bg-primary/90">
|
||||
<Link to="/assessment">Get a free assessment</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-w-md w-full lg:w-auto">
|
||||
<h3 className="text-lg font-semibold text-card-foreground mb-4">Stay updated</h3>
|
||||
<form className="flex gap-2" onSubmit={(e) => e.preventDefault()}>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
className="bg-background border-border text-foreground placeholder:text-muted-foreground"
|
||||
aria-label="Email address"
|
||||
/>
|
||||
<Button type="submit" variant="secondary" className="bg-secondary text-secondary-foreground hover:bg-secondary/80">
|
||||
Subscribe
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Middle Section: Navigation Columns */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-16">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<span className="font-semibold text-card-foreground">Platform</span>
|
||||
<Link to="/platform/argus-grid" className="text-sm text-muted-foreground hover:text-primary transition-colors">The Argus Grid</Link>
|
||||
<Link to="/platform/argus" className="text-sm text-muted-foreground hover:text-primary transition-colors">Argus</Link>
|
||||
<Link to="/platform/external-posture" className="text-sm text-muted-foreground hover:text-primary transition-colors">External Posture</Link>
|
||||
<Link to="/platform/reports-and-evidence" className="text-sm text-muted-foreground hover:text-primary transition-colors">Reports and Evidence</Link>
|
||||
<Link to="/platform/quantum-readiness" className="text-sm text-muted-foreground hover:text-primary transition-colors">Quantum Readiness</Link>
|
||||
<Link to="/platform/roadmap" className="text-sm text-muted-foreground hover:text-primary transition-colors">Roadmap</Link>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<span className="font-semibold text-card-foreground">Solutions</span>
|
||||
<Link to="/solutions/agencies" className="text-sm text-muted-foreground hover:text-primary transition-colors">For Agencies</Link>
|
||||
<Link to="/solutions/mssps" className="text-sm text-muted-foreground hover:text-primary transition-colors">For MSSPs</Link>
|
||||
<Link to="/solutions/businesses" className="text-sm text-muted-foreground hover:text-primary transition-colors">For Businesses</Link>
|
||||
<Link to="/solutions/website-security-audits" className="text-sm text-muted-foreground hover:text-primary transition-colors">Website Security Audits</Link>
|
||||
<Link to="/solutions/managed-secure-hosting" className="text-sm text-muted-foreground hover:text-primary transition-colors">Managed Secure Hosting</Link>
|
||||
<Link to="/solutions/post-quantum-readiness" className="text-sm text-muted-foreground hover:text-primary transition-colors">Post-Quantum Readiness</Link>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<span className="font-semibold text-card-foreground">Resources</span>
|
||||
<Link to="/blog" className="text-sm text-muted-foreground hover:text-primary transition-colors">Blog</Link>
|
||||
<Link to="/resources/sample-report" className="text-sm text-muted-foreground hover:text-primary transition-colors">Sample Report</Link>
|
||||
<Link to="/resources/documentation" className="text-sm text-muted-foreground hover:text-primary transition-colors">Documentation</Link>
|
||||
<Link to="/research" className="text-sm text-muted-foreground hover:text-primary transition-colors">Research and Whitepapers</Link>
|
||||
<Link to="/faq" className="text-sm text-muted-foreground hover:text-primary transition-colors">FAQ</Link>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<span className="font-semibold text-card-foreground">Company</span>
|
||||
<Link to="/about" className="text-sm text-muted-foreground hover:text-primary transition-colors">About</Link>
|
||||
<Link to="/partners" className="text-sm text-muted-foreground hover:text-primary transition-colors">Partners</Link>
|
||||
<Link to="/trust" className="text-sm text-muted-foreground hover:text-primary transition-colors">Trust at Korrith</Link>
|
||||
<Link to="/contact" className="text-sm text-muted-foreground hover:text-primary transition-colors">Contact</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section: Legal & Social */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-6 pt-8 border-t border-border/50 text-sm text-muted-foreground relative z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="relative flex items-center shrink-0 hover:opacity-80 transition-opacity"
|
||||
aria-label="Korrith Home"
|
||||
>
|
||||
<img
|
||||
src="https://horizons-cdn.hostinger.com/f7b2aa49-1077-4692-8b37-dae2a255116c/83c1e4e45b9402680ee713828eebbe4e.png"
|
||||
alt="Korrith"
|
||||
className="h-24 w-24 object-contain -my-9"
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2 z-10">
|
||||
<p>© {currentYear > 2026 ? currentYear : 2026} Korrith. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 z-10">
|
||||
<Link to="/privacy" className="hover:text-card-foreground transition-colors">Privacy Policy</Link>
|
||||
<Link to="/terms" className="hover:text-card-foreground transition-colors">Terms of Service</Link>
|
||||
<div className="flex items-center gap-4 ml-4">
|
||||
<a href="#" aria-label="Twitter" className="hover:text-card-foreground transition-colors"><Twitter className="h-4 w-4" /></a>
|
||||
<a href="#" aria-label="LinkedIn" className="hover:text-card-foreground transition-colors"><Linkedin className="h-4 w-4" /></a>
|
||||
<a href="#" aria-label="GitHub" className="hover:text-card-foreground transition-colors"><Github className="h-4 w-4" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Menu, ChevronDown } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger, SheetTitle } from '@/components/ui/sheet';
|
||||
|
||||
const navigationData = [
|
||||
{
|
||||
title: 'Platform',
|
||||
links: [
|
||||
{ name: 'The Argus Grid', path: '/platform/argus-grid' },
|
||||
{ name: 'Argus', path: '/platform/argus' },
|
||||
{ name: 'External Posture', path: '/platform/external-posture' },
|
||||
{ name: 'Reports and Evidence', path: '/platform/reports-and-evidence' },
|
||||
{ name: 'Quantum Readiness', path: '/platform/quantum-readiness' },
|
||||
{ name: 'Roadmap', path: '/platform/roadmap' },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Solutions',
|
||||
links: [
|
||||
{ name: 'For Agencies', path: '/solutions/agencies' },
|
||||
{ name: 'For MSSPs', path: '/solutions/mssps' },
|
||||
{ name: 'For Businesses', path: '/solutions/businesses' },
|
||||
{ name: 'Website Security Audits', path: '/solutions/website-security-audits' }, // Fixed path
|
||||
{ name: 'Managed Secure Hosting', path: '/solutions/managed-secure-hosting' },
|
||||
{ name: 'Post-Quantum Readiness', path: '/solutions/post-quantum-readiness' },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Why Korrith',
|
||||
path: '/why-korrith',
|
||||
links: []
|
||||
},
|
||||
{
|
||||
title: 'Resources',
|
||||
links: [
|
||||
{ name: 'Blog', path: '/blog' },
|
||||
{ name: 'Sample Report', path: '/resources/sample-report' }, // Fixed path
|
||||
{ name: 'Documentation', path: '/resources/documentation' }, // Fixed path
|
||||
{ name: 'Research and Whitepapers', path: '/research' },
|
||||
{ name: 'FAQ', path: '/faq' },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: [
|
||||
{ name: 'About', path: '/about' },
|
||||
{ name: 'Partners', path: '/partners' },
|
||||
{ name: 'Trust at Korrith', path: '/trust' },
|
||||
{ name: 'Contact', path: '/contact' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const isActive = (path) => location.pathname === path;
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full bg-background/80 backdrop-blur-lg border-b border-border shadow-sm">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
|
||||
{/* Logo Container */}
|
||||
<Link to="/" className="flex items-center shrink-0 transition-opacity hover:opacity-90 group h-full">
|
||||
<div className="relative h-full flex items-center w-36 md:w-48">
|
||||
<img
|
||||
src="https://horizons-cdn.hostinger.com/f7b2aa49-1077-4692-8b37-dae2a255116c/5bf3c331da81252a27cf615c41a0b6b3.png"
|
||||
alt="Korrith"
|
||||
className="absolute max-w-none h-24 md:h-[108px] w-auto left-0 group-hover:scale-105 transition-transform duration-300 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<nav className="hidden lg:flex items-center space-x-1">
|
||||
{navigationData.map((item) => (
|
||||
<div key={item.title} className="relative group px-3 py-2">
|
||||
{item.links.length > 0 ? (
|
||||
<>
|
||||
<button className={`flex items-center gap-1 text-sm font-medium transition-colors ${
|
||||
item.links.some(l => isActive(l.path)) ? 'text-primary' : 'text-muted-foreground hover:text-foreground'
|
||||
}`}>
|
||||
{item.title}
|
||||
<ChevronDown className="h-4 w-4 opacity-50 group-hover:rotate-180 transition-transform duration-200" />
|
||||
</button>
|
||||
<div className="absolute top-full left-0 pt-4 hidden group-hover:block w-64">
|
||||
<div className="bg-card border border-border rounded-lg shadow-xl overflow-hidden p-2 flex flex-col gap-1">
|
||||
{item.links.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className={`px-3 py-2 text-sm rounded-md transition-colors ${
|
||||
isActive(link.path)
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted-foreground hover:bg-white/5 hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
to={item.path}
|
||||
className={`text-sm font-medium transition-colors ${
|
||||
isActive(item.path)
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* CTA & Mobile Toggle */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/login" className="hidden lg:block text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
|
||||
Log in
|
||||
</Link>
|
||||
|
||||
<Button asChild className="hidden md:inline-flex bg-primary text-primary-foreground hover:bg-primary/90 shadow-[0_0_20px_-5px_hsl(var(--primary)/0.4)]">
|
||||
<Link to="/assessment">Get early access</Link>
|
||||
</Button>
|
||||
|
||||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||
<SheetTrigger asChild className="lg:hidden">
|
||||
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground">
|
||||
<Menu className="h-6 w-6" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="w-[300px] bg-card border-l border-border p-6 overflow-y-auto">
|
||||
<SheetTitle className="text-left mb-6 font-bold text-xl text-foreground">
|
||||
Menu
|
||||
</SheetTitle>
|
||||
<nav className="flex flex-col space-y-6">
|
||||
{navigationData.map((item) => (
|
||||
<div key={item.title} className="flex flex-col space-y-3">
|
||||
{item.links.length > 0 ? (
|
||||
<>
|
||||
<div className="text-sm font-semibold text-foreground uppercase tracking-wider">{item.title}</div>
|
||||
<div className="flex flex-col space-y-2 pl-3 border-l border-border">
|
||||
{item.links.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`text-sm transition-colors ${
|
||||
isActive(link.path) ? 'text-primary' : 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
to={item.path}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`text-base font-medium transition-colors ${
|
||||
isActive(item.path) ? 'text-primary' : 'text-foreground hover:text-primary'
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pt-6 border-t border-border mt-6 flex flex-col gap-4">
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-base font-medium text-foreground hover:text-primary transition-colors text-center py-2"
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Button asChild className="w-full bg-primary text-primary-foreground hover:bg-primary/90">
|
||||
<Link to="/assessment" onClick={() => setIsOpen(false)}>Get early access</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import Header from '@/components/Header.jsx';
|
||||
import Footer from '@/components/Footer.jsx';
|
||||
|
||||
export default function LayoutWrapper({ children }) {
|
||||
return (
|
||||
<div className="flex flex-col min-h-[100dvh] bg-background text-foreground">
|
||||
<Header />
|
||||
<main className="flex-grow flex flex-col">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
const ScrollToTop = () => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
||||
|
||||
// Backup GA4 tracking mechanism
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
|
||||
console.log(`[Backup] Sending page_view event to GA4: ${pathname}`);
|
||||
window.gtag('event', 'page_view', {
|
||||
page_path: pathname,
|
||||
page_title: document.title
|
||||
});
|
||||
console.log('[Backup] GA4 page_view event sent successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Backup] Error sending page_view event to GA4:', error);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ScrollToTop;
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronDown
|
||||
className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref} />
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||
{...props} />
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props} />
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as React from "react"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props} />
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props} />
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
||||
{...props} />
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props} />
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from "react"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}) {
|
||||
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Breadcrumb = React.forwardRef(
|
||||
({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />
|
||||
)
|
||||
Breadcrumb.displayName = "Breadcrumb"
|
||||
|
||||
const BreadcrumbList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
BreadcrumbList.displayName = "BreadcrumbList"
|
||||
|
||||
const BreadcrumbItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props} />
|
||||
))
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||
|
||||
const BreadcrumbLink = React.forwardRef(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||
|
||||
const BreadcrumbPage = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
||||
{...props}>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||
vertical:
|
||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function ButtonGroup({
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupText({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupSeparator({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
buttonGroupVariants,
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,175 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayPicker, getDefaultClassNames } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
|
||||
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
|
||||
caption_label: cn("select-none font-medium", captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", defaultClassNames.caption_label),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
||||
week_number_header: cn("w-[--cell-size] select-none", defaultClassNames.week_number_header),
|
||||
week_number: cn(
|
||||
"text-muted-foreground select-none text-[0.8rem]",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (<div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />);
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (<ChevronLeftIcon className={cn("size-4", className)} {...props} />);
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (<ChevronRightIcon className={cn("size-4", className)} {...props} />);
|
||||
}
|
||||
|
||||
return (<ChevronDownIcon className={cn("size-4", className)} {...props} />);
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div
|
||||
className="flex size-[--cell-size] items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
||||
{...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
const CarouselContext = React.createContext(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef((
|
||||
{
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel({
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
}, plugins)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api) => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback((event) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
}, [scrollPrev, scrollNext])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return
|
||||
}
|
||||
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
};
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
})
|
||||
Carousel.displayName = "Carousel"
|
||||
|
||||
const CarouselContent = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
CarouselContent.displayName = "CarouselContent"
|
||||
|
||||
const CarouselItem = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
CarouselItem.displayName = "CarouselItem"
|
||||
|
||||
const CarouselPrevious = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", className)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
})
|
||||
CarouselPrevious.displayName = "CarouselPrevious"
|
||||
|
||||
const CarouselNext = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
})
|
||||
CarouselNext.displayName = "CarouselNext"
|
||||
|
||||
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
|
||||
@@ -0,0 +1,312 @@
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = {
|
||||
light: "",
|
||||
dark: ".dark"
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
})
|
||||
ChartContainer.displayName = "Chart"
|
||||
|
||||
const ChartStyle = ({
|
||||
id,
|
||||
config
|
||||
}) => {
|
||||
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`)
|
||||
.join("\n"),
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
const ChartTooltipContent = React.forwardRef((
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
})}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor
|
||||
}
|
||||
} />
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
const ChartLegendContent = React.forwardRef((
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}>
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||
)}>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}} />
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config,
|
||||
payload,
|
||||
key
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key]
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[key]
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
|
||||
const Command = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
const CommandDialog = ({ children, ...props }) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[data-cmdk-input-wrapper]_svg]:h-5 [&_[data-cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" data-cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({ className, ...props }) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import * as React from "react"
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ContextMenu = ContextMenuPrimitive.Root
|
||||
|
||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
||||
|
||||
const ContextMenuGroup = ContextMenuPrimitive.Group
|
||||
|
||||
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
||||
|
||||
const ContextMenuSub = ContextMenuPrimitive.Sub
|
||||
|
||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
||||
|
||||
const ContextMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
))
|
||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const ContextMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
||||
|
||||
const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</ContextMenuPrimitive.Portal>
|
||||
))
|
||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
||||
|
||||
const ContextMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
||||
|
||||
const ContextMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
ContextMenuCheckboxItem.displayName =
|
||||
ContextMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const ContextMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-4 w-4 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
))
|
||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const ContextMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
||||
|
||||
const ContextMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props} />
|
||||
))
|
||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
||||
|
||||
const ContextMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
||||
{...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props} />
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}) => (
|
||||
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
|
||||
)
|
||||
Drawer.displayName = "Drawer"
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close
|
||||
|
||||
const DrawerOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props} />
|
||||
))
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||
|
||||
const DrawerContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
))
|
||||
DrawerContent.displayName = "DrawerContent"
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props} />
|
||||
)
|
||||
DrawerHeader.displayName = "DrawerHeader"
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
|
||||
)
|
||||
DrawerFooter.displayName = "DrawerFooter"
|
||||
|
||||
const DrawerTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||
|
||||
const DrawerDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props} />
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props} />
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Empty({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty"
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance rounded-lg border-dashed p-6 text-center md:p-12",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyHeader({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-header"
|
||||
className={cn("flex max-w-sm flex-col items-center gap-2 text-center", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMediaVariants = cva(
|
||||
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function EmptyMedia({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-icon"
|
||||
data-variant={variant}
|
||||
className={cn(emptyMediaVariants({ variant, className }))}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyTitle({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-title"
|
||||
className={cn("text-lg font-medium tracking-tight", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyDescription({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-description"
|
||||
className={cn(
|
||||
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyContent({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-content"
|
||||
className={cn(
|
||||
"flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Empty,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
EmptyDescription,
|
||||
EmptyContent,
|
||||
EmptyMedia,
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { useMemo } from "react"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
function FieldSet({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<fieldset
|
||||
data-slot="field-set"
|
||||
className={cn(
|
||||
"flex flex-col gap-6",
|
||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLegend({
|
||||
className,
|
||||
variant = "legend",
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<legend
|
||||
data-slot="field-legend"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"mb-3 font-medium",
|
||||
"data-[variant=legend]:text-base",
|
||||
"data-[variant=label]:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function FieldGroup({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-group"
|
||||
className={cn(
|
||||
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const fieldVariants = cva("group/field data-[invalid=true]:text-destructive flex w-full gap-3", {
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
||||
horizontal: [
|
||||
"flex-row items-center",
|
||||
"[&>[data-slot=field-label]]:flex-auto",
|
||||
"has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start",
|
||||
],
|
||||
responsive: [
|
||||
"@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto",
|
||||
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
||||
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
},
|
||||
})
|
||||
|
||||
function Field({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="field"
|
||||
data-orientation={orientation}
|
||||
className={cn(fieldVariants({ orientation }), className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function FieldContent({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-content"
|
||||
className={cn("group/field-content flex flex-1 flex-col gap-1.5 leading-snug", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Label
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4",
|
||||
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function FieldTitle({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function FieldDescription({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm font-normal leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
||||
"nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5",
|
||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function FieldSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-separator"
|
||||
data-content={!!children}
|
||||
className={cn(
|
||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<Separator className="absolute inset-0 top-1/2" />
|
||||
{children && (
|
||||
<span
|
||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||
data-slot="field-separator-content">
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldError({
|
||||
className,
|
||||
children,
|
||||
errors,
|
||||
...props
|
||||
}) {
|
||||
const content = useMemo(() => {
|
||||
if (children) {
|
||||
return children
|
||||
}
|
||||
|
||||
if (!errors) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (errors?.length === 1 && errors[0]?.message) {
|
||||
return errors[0].message
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||
{errors.map((error, index) =>
|
||||
error?.message && <li key={index}>{error.message}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}, [children, errors])
|
||||
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
className={cn("text-destructive text-sm font-normal", className)}
|
||||
{...props}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldContent,
|
||||
FieldTitle,
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { Controller, FormProvider, useFormContext } from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
const FormFieldContext = React.createContext(null)
|
||||
|
||||
const FormField = (
|
||||
{
|
||||
...props
|
||||
}
|
||||
) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
if (!itemContext) {
|
||||
throw new Error("useFormField should be used within <FormItem>")
|
||||
}
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext(null)
|
||||
|
||||
const FormItem = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
@@ -0,0 +1,160 @@
|
||||
import * as React from "react"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
function InputGroup({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]",
|
||||
"h-9 has-[>textarea]:h-auto",
|
||||
// Variants based on alignment.
|
||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
||||
// Focus state.
|
||||
"has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1",
|
||||
// Error state.
|
||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
"inline-start":
|
||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
||||
"inline-end":
|
||||
"order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]",
|
||||
"block-start":
|
||||
"[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5",
|
||||
"block-end":
|
||||
"[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function InputGroupAddon({
|
||||
className,
|
||||
align = "inline-start",
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
data-align={align}
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
onClick={(e) => {
|
||||
if ((e.target).closest("button")) {
|
||||
return
|
||||
}
|
||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
||||
}}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", {
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
||||
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xs",
|
||||
},
|
||||
})
|
||||
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = "button",
|
||||
variant = "ghost",
|
||||
size = "xs",
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupText({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupInput({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupTextarea({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupInput,
|
||||
InputGroupTextarea,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { Minus } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const InputOTP = React.forwardRef(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props} />
|
||||
))
|
||||
InputOTP.displayName = "InputOTP"
|
||||
|
||||
const InputOTPGroup = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
))
|
||||
InputOTPGroup.displayName = "InputOTPGroup"
|
||||
|
||||
const InputOTPSlot = React.forwardRef(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-1 ring-ring",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
InputOTPSlot.displayName = "InputOTPSlot"
|
||||
|
||||
const InputOTPSeparator = React.forwardRef(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Minus />
|
||||
</div>
|
||||
))
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,197 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
function ItemGroup({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="list"
|
||||
data-slot="item-group"
|
||||
className={cn("group/item-group flex flex-col", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ItemSeparator({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="item-separator"
|
||||
orientation="horizontal"
|
||||
className={cn("my-0", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const itemVariants = cva(
|
||||
"group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 [a]:transition-colors flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline: "border-border",
|
||||
muted: "bg-muted/50",
|
||||
},
|
||||
size: {
|
||||
default: "gap-4 p-4 ",
|
||||
sm: "gap-2.5 px-4 py-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Item({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
return (
|
||||
<Comp
|
||||
data-slot="item"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(itemVariants({ variant, size, className }))}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const itemMediaVariants = cva(
|
||||
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
icon: "bg-muted size-8 rounded-sm border [&_svg:not([class*='size-'])]:size-4",
|
||||
image:
|
||||
"size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function ItemMedia({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-media"
|
||||
data-variant={variant}
|
||||
className={cn(itemMediaVariants({ variant, className }))}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ItemContent({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-content"
|
||||
className={cn(
|
||||
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ItemTitle({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-title"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 text-sm font-medium leading-snug",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ItemDescription({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<p
|
||||
data-slot="item-description"
|
||||
className={cn(
|
||||
"text-muted-foreground line-clamp-2 text-balance text-sm font-normal leading-normal",
|
||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ItemActions({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-actions"
|
||||
className={cn("flex items-center gap-2", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ItemHeader({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-header"
|
||||
className={cn("flex basis-full items-center justify-between gap-2", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ItemFooter({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-footer"
|
||||
className={cn("flex basis-full items-center justify-between gap-2", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Item,
|
||||
ItemMedia,
|
||||
ItemContent,
|
||||
ItemActions,
|
||||
ItemGroup,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
ItemDescription,
|
||||
ItemHeader,
|
||||
ItemFooter,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Kbd({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium",
|
||||
"[&_svg:not([class*='size-'])]:size-3",
|
||||
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function KbdGroup({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd-group"
|
||||
className={cn("inline-flex items-center gap-1", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Kbd, KbdGroup }
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,198 @@
|
||||
import * as React from "react"
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function MenubarMenu({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.Menu {...props} />;
|
||||
}
|
||||
|
||||
function MenubarGroup({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.Group {...props} />;
|
||||
}
|
||||
|
||||
function MenubarPortal({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.Portal {...props} />;
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.RadioGroup {...props} />;
|
||||
}
|
||||
|
||||
function MenubarSub({
|
||||
...props
|
||||
}) {
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
||||
}
|
||||
|
||||
const Menubar = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName
|
||||
|
||||
const MenubarTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
))
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
||||
|
||||
const MenubarSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
||||
|
||||
const MenubarContent = React.forwardRef((
|
||||
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
||||
ref
|
||||
) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</MenubarPrimitive.Portal>
|
||||
))
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
||||
|
||||
const MenubarItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
))
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
||||
|
||||
const MenubarRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Circle className="h-4 w-4 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
))
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
||||
|
||||
const MenubarLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
||||
|
||||
const MenubarSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props} />
|
||||
))
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
||||
|
||||
const MenubarShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
MenubarShortcut.displayname = "MenubarShortcut"
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const NavigationMenu = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
))
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||
|
||||
const NavigationMenuList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
||||
)
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true" />
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
))
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||
|
||||
const NavigationMenuContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
</div>
|
||||
))
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<div
|
||||
className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
))
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName
|
||||
|
||||
export {
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
const Pagination = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props} />
|
||||
)
|
||||
Pagination.displayName = "Pagination"
|
||||
|
||||
const PaginationContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props} />
|
||||
))
|
||||
PaginationContent.displayName = "PaginationContent"
|
||||
|
||||
const PaginationItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("", className)} {...props} />
|
||||
))
|
||||
PaginationItem.displayName = "PaginationItem"
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}), className)}
|
||||
{...props} />
|
||||
)
|
||||
PaginationLink.displayName = "PaginationLink"
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
{...props}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = "PaginationPrevious"
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
{...props}>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationNext.displayName = "PaginationNext"
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} />
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const RadioGroup = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (<RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />);
|
||||
})
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||
|
||||
const RadioGroupItem = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-3.5 w-3.5 fill-primary" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
})
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
import { GripVertical } from "lucide-react";
|
||||
import { Group, Panel, Separator } from "react-resizable-panels";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const PanelGroupDirectionContext = createContext("horizontal");
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
direction,
|
||||
orientation = direction ?? "horizontal",
|
||||
...props
|
||||
}) => (
|
||||
<PanelGroupDirectionContext.Provider value={orientation}>
|
||||
<Group
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className,
|
||||
)}
|
||||
data-panel-group-direction={orientation}
|
||||
orientation={orientation}
|
||||
{...props}
|
||||
/>
|
||||
</PanelGroupDirectionContext.Provider>
|
||||
);
|
||||
|
||||
const ResizablePanel = Panel;
|
||||
|
||||
const ResizableHandle = ({ withHandle, className, ...props }) => {
|
||||
const direction = useContext(PanelGroupDirectionContext);
|
||||
return (
|
||||
<Separator
|
||||
className={cn(
|
||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className,
|
||||
)}
|
||||
data-panel-group-direction={direction}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||
<GripVertical className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</Separator>
|
||||
);
|
||||
};
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -0,0 +1,119 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn("p-1", position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props} />
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props} />
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef((
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref} />
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const SheetContent = React.forwardRef(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||
<SheetPrimitive.Close
|
||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||
{...props} />
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props} />
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
const SidebarContext = React.createContext(null)
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const SidebarProvider = React.forwardRef((
|
||||
{
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback((value) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
}, [setOpenProp, open])
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar])
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo(() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar])
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style
|
||||
}
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
})
|
||||
SidebarProvider.displayName = "SidebarProvider"
|
||||
|
||||
const Sidebar = React.forwardRef((
|
||||
{
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE
|
||||
}
|
||||
}
|
||||
side={side}>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="group peer hidden text-sidebar-foreground md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
||||
)} />
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
Sidebar.displayName = "Sidebar"
|
||||
|
||||
const SidebarTrigger = React.forwardRef(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-7 w-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
}}
|
||||
{...props}>
|
||||
<PanelLeft />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
})
|
||||
SidebarTrigger.displayName = "SidebarTrigger"
|
||||
|
||||
const SidebarRail = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
data-sidebar="rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
|
||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarRail.displayName = "SidebarRail"
|
||||
|
||||
const SidebarInset = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 flex-col bg-background",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarInset.displayName = "SidebarInset"
|
||||
|
||||
const SidebarInput = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarInput.displayName = "SidebarInput"
|
||||
|
||||
const SidebarHeader = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarHeader.displayName = "SidebarHeader"
|
||||
|
||||
const SidebarFooter = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarFooter.displayName = "SidebarFooter"
|
||||
|
||||
const SidebarSeparator = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Separator
|
||||
ref={ref}
|
||||
data-sidebar="separator"
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarSeparator.displayName = "SidebarSeparator"
|
||||
|
||||
const SidebarContent = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarContent.displayName = "SidebarContent"
|
||||
|
||||
const SidebarGroup = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarGroup.displayName = "SidebarGroup"
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel"
|
||||
|
||||
const SidebarGroupAction = React.forwardRef(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction"
|
||||
|
||||
const SidebarGroupContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarGroupContent.displayName = "SidebarGroupContent"
|
||||
|
||||
const SidebarMenu = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarMenu.displayName = "SidebarMenu"
|
||||
|
||||
const SidebarMenuItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const SidebarMenuButton = React.forwardRef((
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props} />
|
||||
)
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip} />
|
||||
</Tooltip>
|
||||
);
|
||||
})
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton"
|
||||
|
||||
const SidebarMenuAction = React.forwardRef(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction"
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge"
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}>
|
||||
{showIcon && (
|
||||
<Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-[--skeleton-width] flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width
|
||||
}
|
||||
} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
|
||||
|
||||
const SidebarMenuSub = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
SidebarMenuSub.displayName = "SidebarMenuSub"
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef(({ ...props }, ref) => <li ref={ref} {...props} />)
|
||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef(
|
||||
({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
)
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||
{...props}>
|
||||
<SliderPrimitive.Track
|
||||
className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb
|
||||
className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
const Toaster = ({
|
||||
...props
|
||||
}) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Loader2Icon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Spinner({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cn("size-4 animate-spin", className)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Spinner }
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)} />
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,86 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props} />
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props} />
|
||||
);
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||
{...props} />
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction };
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
import * as React from "react"
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { toggleVariants } from "@/components/ui/toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
})
|
||||
|
||||
const ToggleGroup = React.forwardRef(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("flex items-center justify-center gap-1", className)}
|
||||
{...props}>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
))
|
||||
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
||||
|
||||
const ToggleGroupItem = React.forwardRef(({ className, children, variant, size, ...props }, ref) => {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}), className)}
|
||||
{...props}>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
})
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toggle = React.forwardRef(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props} />
|
||||
))
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
</TooltipPrimitive.Portal>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client";
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST"
|
||||
}
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map()
|
||||
|
||||
const addToRemoveQueue = (toastId) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t),
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t),
|
||||
};
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const listeners = []
|
||||
|
||||
let memoryState = { toasts: [] }
|
||||
|
||||
function dispatch(action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
function toast({
|
||||
...props
|
||||
}) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
};
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useGoogleAnalytics = () => {
|
||||
const measurementId = 'G-3DTN0F457M';
|
||||
|
||||
const initializeGA = useCallback(() => {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
|
||||
console.log(`GA4 initialized with measurement ID ${measurementId}`);
|
||||
} else {
|
||||
console.warn('GA4 gtag is not defined on window object yet.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing GA4:', error);
|
||||
}
|
||||
}, [measurementId]);
|
||||
|
||||
const trackPageView = useCallback((path, title) => {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
|
||||
console.log(`Sending page_view event to GA4: ${path}`);
|
||||
window.gtag('event', 'page_view', {
|
||||
page_title: title || document.title,
|
||||
page_location: window.location.href,
|
||||
page_path: path,
|
||||
});
|
||||
console.log('GA4 page_view event sent successfully');
|
||||
} else {
|
||||
console.error('Cannot send page_view: gtag is not defined.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending page_view event to GA4:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { initializeGA, trackPageView };
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Korrith Premium Dark Theme */
|
||||
--background: 227 43% 4%; /* #06080f */
|
||||
--card: 222 37% 7%; /* #0b0f18 */
|
||||
--foreground: 220 36% 93%; /* #e7ecf6 */
|
||||
|
||||
--card-foreground: 220 36% 93%;
|
||||
--popover: 222 37% 7%;
|
||||
--popover-foreground: 220 36% 93%;
|
||||
|
||||
--primary: 219 100% 65%; /* #4d8bff */
|
||||
--primary-foreground: 227 43% 4%;
|
||||
|
||||
--secondary: 222 37% 12%;
|
||||
--secondary-foreground: 220 36% 93%;
|
||||
|
||||
--muted: 222 37% 12%;
|
||||
--muted-foreground: 220 36% 70%;
|
||||
|
||||
--accent: 222 37% 15%;
|
||||
--accent-foreground: 220 36% 93%;
|
||||
|
||||
--destructive: 356 83% 68%; /* #f06a72 */
|
||||
--destructive-foreground: 227 43% 4%;
|
||||
|
||||
--border: 222 37% 15%;
|
||||
--input: 222 37% 15%;
|
||||
--ring: 219 100% 65%;
|
||||
|
||||
--radius: 8px;
|
||||
|
||||
/* Korrith Custom Accents */
|
||||
--accent-ai: 178 73% 63%; /* #5fe3e0 */
|
||||
--accent-green: 160 61% 53%; /* #3ad29f */
|
||||
--accent-amber: 40 89% 60%; /* #f4b740 */
|
||||
--accent-red: 356 83% 68%; /* #f06a72 */
|
||||
--accent-purple: 270 80% 65%; /* #9b4dff */
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Retain same values since it's a dedicated dark theme system */
|
||||
--background: 227 43% 4%;
|
||||
--card: 222 37% 7%;
|
||||
--foreground: 220 36% 93%;
|
||||
--card-foreground: 220 36% 93%;
|
||||
--popover: 222 37% 7%;
|
||||
--popover-foreground: 220 36% 93%;
|
||||
--primary: 219 100% 65%;
|
||||
--primary-foreground: 227 43% 4%;
|
||||
--secondary: 222 37% 12%;
|
||||
--secondary-foreground: 220 36% 93%;
|
||||
--muted: 222 37% 12%;
|
||||
--muted-foreground: 220 36% 70%;
|
||||
--accent: 222 37% 15%;
|
||||
--accent-foreground: 220 36% 93%;
|
||||
--destructive: 356 83% 68%;
|
||||
--destructive-foreground: 227 43% 4%;
|
||||
--border: 222 37% 15%;
|
||||
--input: 222 37% 15%;
|
||||
--ring: 219 100% 65%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
overflow-x: hidden;
|
||||
font-feature-settings: "kern" 1, "liga" 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
text-wrap: balance;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
line-height: 1.625;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.font-mono {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
}
|
||||
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.hairline-border {
|
||||
border: 1px solid hsl(var(--border) / 0.5);
|
||||
}
|
||||
|
||||
.hairline-border-t {
|
||||
border-top: 1px solid hsl(var(--border) / 0.5);
|
||||
}
|
||||
|
||||
.hairline-border-b {
|
||||
border-bottom: 1px solid hsl(var(--border) / 0.5);
|
||||
}
|
||||
|
||||
.radial-glow {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.radial-glow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at center, hsl(var(--primary) / 0.15) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.radial-glow-top::before {
|
||||
top: 0;
|
||||
width: 150%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle at top center, hsl(var(--primary) / 0.1) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.radial-glow-accent::before {
|
||||
background: radial-gradient(circle at center, hsl(var(--accent-ai) / 0.15) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.radial-glow-purple::before {
|
||||
background: radial-gradient(circle at center, hsl(var(--accent-purple) / 0.15) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.transition-korrith {
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const API_SERVER_URL = '/hcgi/api';
|
||||
|
||||
const apiServerClient = {
|
||||
fetch: async (url, options = {}) => {
|
||||
return await window.fetch(API_SERVER_URL + url, options);
|
||||
}
|
||||
};
|
||||
|
||||
export default apiServerClient;
|
||||
|
||||
export { apiServerClient };
|
||||
@@ -0,0 +1,9 @@
|
||||
import Pocketbase from 'pocketbase';
|
||||
|
||||
const POCKETBASE_API_URL = '/hcgi/platform';
|
||||
|
||||
const pocketbaseClient = new Pocketbase(POCKETBASE_API_URL);
|
||||
|
||||
export default pocketbaseClient;
|
||||
|
||||
export { pocketbaseClient };
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.jsx';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<App />
|
||||
);
|
||||
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ShieldCheck, Lock, FileText, Search } from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-100px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-100px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>About Korrith | Security You Can Prove</title>
|
||||
<meta name="description" content="Korrith exists to make security something you can prove, not just describe. The conviction behind the product and where we are taking it." />
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 md:pt-40 md:pb-32 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.08),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--accent-purple)/0.08),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-6 leading-[1.1]">
|
||||
Security you can prove.
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-2xl">
|
||||
Korrith exists to make security something you can prove, not just describe. The conviction behind the product and where we are taking it.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Problem Section */}
|
||||
<section className="py-24 bg-card/20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp} className="max-w-3xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-foreground">The problem we set out to solve</h2>
|
||||
<div className="prose prose-lg prose-invert max-w-none text-muted-foreground space-y-6">
|
||||
<p>
|
||||
Organizations manage growing numbers of domains, IP addresses, and shifting cloud configurations. That sprawl creates an external attack surface that is hard to monitor continuously. Misconfigured headers, expiring certificates, and exposed interfaces stay common.
|
||||
</p>
|
||||
<p>
|
||||
Security teams are drowning in findings. Most tools flag everything they see, regardless of context, leaving engineers to sift through thousands of false positives and theoretical vulnerabilities.
|
||||
We saw firsthand how this erodes trust between security and engineering. When an alert fires, it should mean something is actually wrong.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Choices Section */}
|
||||
<section className="py-24 border-y border-border/50 relative overflow-hidden bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div {...fadeInUp} className="mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-foreground">The choices we made</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl">
|
||||
To fix the disconnect between raw data and actual security posture, we built Korrith on a strict set of principles.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||
>
|
||||
{[
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "Honest by construction",
|
||||
text: "We made our AI honest by construction, so it cannot invent risk."
|
||||
},
|
||||
{
|
||||
icon: Lock,
|
||||
title: "Safe by construction",
|
||||
text: "We made our active testing safe by construction, so it cannot act outside signed authorization."
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
title: "Audit-grade reporting",
|
||||
text: "We made our reporting audit-grade by default, so the truth the platform holds is the document you export."
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
title: "Evidence-backed results",
|
||||
text: "We refused to ever show a clean result we could not back with evidence."
|
||||
}
|
||||
].map((choice, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={fadeInUp}
|
||||
className="bg-card border border-border/60 rounded-2xl p-8 hover:border-primary/50 transition-korrith group flex flex-col h-full shadow-sm hover:shadow-lg hover:shadow-primary/5"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-secondary flex items-center justify-center text-primary mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||
<choice.icon size={24} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-card-foreground mb-3">{choice.title}</h3>
|
||||
<p className="text-muted-foreground leading-relaxed text-lg">
|
||||
{choice.text}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Where We Are Going Section */}
|
||||
<section className="py-24 bg-card/30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp} className="max-w-3xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-foreground">Where we are going</h2>
|
||||
<div className="prose prose-lg prose-invert max-w-none text-muted-foreground space-y-6">
|
||||
<p>
|
||||
At the same time, the cryptography protecting the world's data faces a historic shift. As quantum computing advances, standard public-key cryptography will eventually become vulnerable. Moving to new standards takes years of planning, inventory, and execution.
|
||||
</p>
|
||||
<p>
|
||||
Korrith is being built to address both immediate misconfigurations and long-term systemic shifts: a continuous loop of external risk assessment combined with a structured path to post-quantum migration. We are building the engine to map your external assets safely, categorize services, and surface common exposure with no disruptive testing.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 relative overflow-hidden border-t border-border/50 bg-secondary/30">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.05),transparent_60%)] pointer-events-none"></div>
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-6">Experience security you can prove.</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10 max-w-2xl mx-auto">
|
||||
Join the organizations that are securing their infrastructure and preparing for a quantum future with Korrith.
|
||||
</p>
|
||||
<Button asChild size="lg" className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-korrith">
|
||||
<Link to="/assessment">
|
||||
Get a free assessment
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
ShieldCheck,
|
||||
Activity,
|
||||
Key,
|
||||
DoorOpen,
|
||||
PowerOff,
|
||||
Server,
|
||||
ArrowRight,
|
||||
Shield,
|
||||
Network
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.1 }
|
||||
};
|
||||
|
||||
export default function ActiveValidationPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Active Validation | Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Korrith actively and safely confirms which exposures are real and exploitable, under a cryptographic authorization gate and a kill-switch. Proof, not theory."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[80vh] flex flex-col justify-center pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--primary)/0.05),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<ShieldCheck className="w-4 h-4 text-primary" />
|
||||
Active Validation Engine
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 leading-[1.1]">
|
||||
Prove what is exploitable, safely.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-12">
|
||||
A detected finding is a hypothesis. Active validation turns it into proof: Korrith actively confirms which exposures are genuinely reachable and exploitable, so you act on what an attacker could really use, not on theoretical noise. And it does this under a cryptographic authorization gate that makes the testing safe by construction.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/contact">
|
||||
Talk to us about active validation
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What it confirms Section (Numbered/Icon Stack layout) */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp} className="mb-16 max-w-2xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
|
||||
What it confirms
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Moving beyond raw metrics to concrete proof of exploitability.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 max-w-4xl">
|
||||
{[
|
||||
{
|
||||
icon: Activity,
|
||||
title: "Reachability",
|
||||
desc: "Non-destructive checks that confirm an exposure is genuinely live and reachable, turning a detected finding into a confirmed one, with no exploitation and no disruption."
|
||||
},
|
||||
{
|
||||
icon: Key,
|
||||
title: "Weak, default, and reused credentials",
|
||||
desc: "Under explicit authorization, controlled and lockout-safe checks for credentials that should not work but do, the actual vulnerability behind most real intrusions."
|
||||
},
|
||||
{
|
||||
icon: DoorOpen,
|
||||
title: "Broken access controls",
|
||||
desc: "Non-destructive confirmation that a protected resource can be reached when it should not be."
|
||||
}
|
||||
].map((item, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="flex flex-col sm:flex-row gap-6 p-8 rounded-2xl bg-card border border-border/60 hover:border-primary/40 transition-colors group"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border group-hover:bg-primary/10 transition-colors">
|
||||
<item.icon className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-foreground mb-3">{item.title}</h3>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Safe by construction Bento Grid Section */}
|
||||
<section className="py-24 bg-secondary/20 border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp} className="mb-16 max-w-3xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
Safe by construction, not by promise
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
This is the part most tools cannot offer responsibly. Korrith can, because of how it is built:
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||||
>
|
||||
{/* Card 1 */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-card border border-border/60 rounded-3xl p-8 md:p-10 relative overflow-hidden group"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 group-hover:bg-primary/10 transition-colors"></div>
|
||||
<Shield className="w-10 h-10 text-primary mb-6 relative z-10" />
|
||||
<h3 className="text-2xl font-bold text-foreground mb-4 relative z-10">
|
||||
The authorization gate
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed relative z-10">
|
||||
Validation only proceeds under cryptographically signed authorization. The system is designed to fail closed: without explicit, verified permission, the engine simply cannot act.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Card 2 */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-card border border-border/60 rounded-3xl p-8 md:p-10 relative overflow-hidden group"
|
||||
>
|
||||
<div className="absolute top-0 left-0 w-64 h-64 bg-emerald-500/5 rounded-full blur-3xl -translate-y-1/2 -translate-x-1/2 group-hover:bg-emerald-500/10 transition-colors"></div>
|
||||
<ShieldCheck className="w-10 h-10 text-emerald-400 mb-6 relative z-10" />
|
||||
<h3 className="text-2xl font-bold text-foreground mb-4 relative z-10">
|
||||
Do no harm
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed relative z-10">
|
||||
Credential checks are strictly metered to stay well under lockout thresholds. The engine would rather miss a weak credential than lock a critical account, and it masks its fingerprints to prevent unintended alerts.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Card 3 */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-card border border-border/60 rounded-3xl p-8 md:p-10 relative overflow-hidden group"
|
||||
>
|
||||
<div className="absolute bottom-0 right-0 w-64 h-64 bg-rose-500/5 rounded-full blur-3xl translate-y-1/2 translate-x-1/2 group-hover:bg-rose-500/10 transition-colors"></div>
|
||||
<PowerOff className="w-10 h-10 text-rose-400 mb-6 relative z-10" />
|
||||
<h3 className="text-2xl font-bold text-foreground mb-4 relative z-10">
|
||||
The kill-switch
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed relative z-10">
|
||||
You maintain absolute control over the validation engine. An instant halt and reset control is permanently available, immediately terminating any active validation task.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Card 4 */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-card border border-border/60 rounded-3xl p-8 md:p-10 relative overflow-hidden group"
|
||||
>
|
||||
<div className="absolute bottom-0 left-0 w-64 h-64 bg-blue-500/5 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2 group-hover:bg-blue-500/10 transition-colors"></div>
|
||||
<Server className="w-10 h-10 text-blue-400 mb-6 relative z-10" />
|
||||
<h3 className="text-2xl font-bold text-foreground mb-4 relative z-10">
|
||||
Owned assets, staging first
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed relative z-10">
|
||||
Validation is strictly tied to asset ownership and engagement-based activation. We default to proving the system on staging and explicitly owned infrastructure before touching sensitive environments.
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Argus Orchestrates & Status Section */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16">
|
||||
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="bg-secondary/30 rounded-3xl p-8 md:p-12 border border-border/50"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Network className="w-6 h-6 text-primary" />
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground">
|
||||
Argus orchestrates, the gate acts
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Argus reasons about which exposures are worth validating and in what order, but only the gated execution engine acts, and only inside the authorization. Reasoning and action are separated on purpose, with the kill-switch sitting at the action boundary.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="bg-secondary/30 rounded-3xl p-8 md:p-12 border border-border/50"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Activity className="w-6 h-6 text-primary" />
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground">
|
||||
Status
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mb-6">
|
||||
Active validation is built, and available on your own assets first, which is how we prove it before it ever points anywhere else.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Client-facing active testing is gated behind signed active-testing terms reviewed by counsel, because the authorization is the entire point and we will not shortcut it.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Final CTA Section */}
|
||||
<section className="py-24 bg-card/20 border-t border-border/50 text-center">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp}>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
Ready to see real proof?
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10">
|
||||
Stop chasing theoretical metrics and start validating what actually matters.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/contact">
|
||||
Talk to us about active validation
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Building2,
|
||||
BrainCircuit,
|
||||
FolderTree,
|
||||
Power,
|
||||
Microscope,
|
||||
Repeat,
|
||||
ArrowRight,
|
||||
ShieldAlert,
|
||||
Briefcase
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
const whyItWorks = [
|
||||
{
|
||||
icon: BrainCircuit,
|
||||
title: "No security expertise required.",
|
||||
description: "Argus does the reasoning and writes the plain-language explanation. You deliver insight, not jargon.",
|
||||
color: "text-blue-400",
|
||||
bg: "bg-blue-400/10 border-blue-400/20"
|
||||
},
|
||||
{
|
||||
icon: FolderTree,
|
||||
title: "Multi-client by design.",
|
||||
description: "Scope, assess, and report per client, with strict boundaries between them. Each client sees only their own posture; you see your whole book in one place.",
|
||||
color: "text-purple-400",
|
||||
bg: "bg-purple-400/10 border-purple-400/20"
|
||||
},
|
||||
{
|
||||
icon: Power,
|
||||
title: "Start with no install.",
|
||||
description: "The external assessment needs nothing from the client to begin, so you can lead with value before asking for access.",
|
||||
color: "text-emerald-400",
|
||||
bg: "bg-emerald-400/10 border-emerald-400/20"
|
||||
},
|
||||
{
|
||||
icon: Microscope,
|
||||
title: "Go deeper when they are ready.",
|
||||
description: "A read-only, outbound-only plugin or agent gives host-internal depth for clients who want a real audit, still with no risk to their site.",
|
||||
color: "text-amber-400",
|
||||
bg: "bg-amber-400/10 border-amber-400/20"
|
||||
}
|
||||
];
|
||||
|
||||
export default function AgenciesPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Korrith for Agencies | A Security Line You Can Actually Sell</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Agencies that build and manage client sites can add real security assessment and assurance, with per-client reporting, a recurring fee, and no security team required."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 md:pt-40 md:pb-32 overflow-hidden border-b border-border/50 min-h-[85vh] flex items-center">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,rgba(139,92,246,0.1),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<Building2 className="w-4 h-4 text-primary" />
|
||||
For Agencies
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
Offer your clients security you can prove, without becoming a security firm.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-10">
|
||||
Your clients trust you with their websites and infrastructure. Security is the question they do not know how to ask and you have not had a clean way to answer. Korrith gives you that answer: assess each client's real exposure, validate what matters, and hand them proof, all under your relationship and your brand of service.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group">
|
||||
<Link to="/contact">
|
||||
Become an agency partner
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Opportunity Section */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-8 text-balance">
|
||||
The opportunity hiding in your client base
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed">
|
||||
Every client you host or maintain is exposed in ways neither of you can currently see. With Korrith you can run a free external assessment on any client domain you manage and authorize, and show them, in plain language, what an adversary sees. That is the easiest security conversation you will ever have, and it opens a recurring line of revenue that does not require you to hire a security team.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why it works for an agency */}
|
||||
<section className="py-24 bg-secondary/20 border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="mb-16 max-w-3xl"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Why it works for an agency specifically
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8"
|
||||
>
|
||||
{whyItWorks.map((item, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
variants={fadeInUp}
|
||||
className="group relative flex flex-col bg-card hover:bg-secondary/40 border border-border rounded-3xl p-8 transition-all duration-300"
|
||||
>
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center border mb-6 ${item.bg} group-hover:scale-105 transition-transform duration-300`}>
|
||||
<item.icon className={`w-7 h-7 ${item.color}`} />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold text-foreground mb-3 group-hover:text-primary transition-colors">
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground leading-relaxed text-lg">
|
||||
{item.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Flowing Value Props: Recurring & Assess */}
|
||||
<section className="py-24 bg-background border-b border-border/50 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Recurring */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center mb-32">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center mb-8">
|
||||
<Repeat className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Recurring, not one-time
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
A scan is a one-off. Assurance is a relationship. Korrith runs the loop continuously and proves resolution over time, which means you can charge a recurring fee for something your clients can see working month after month, and show them evidence they can forward to their own customers.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="relative aspect-square lg:aspect-auto lg:h-[400px] rounded-3xl bg-secondary/50 border border-border flex items-center justify-center p-8 overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.05),transparent_70%)] pointer-events-none"></div>
|
||||
{/* Abstract decorative element representing loop/recurring */}
|
||||
<div className="relative w-48 h-48">
|
||||
<div className="absolute inset-0 rounded-full border-4 border-primary/20 animate-[spin_10s_linear_infinite]"></div>
|
||||
<div className="absolute inset-4 rounded-full border-4 border-dashed border-primary/40 animate-[spin_15s_linear_infinite_reverse]"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Repeat className="w-12 h-12 text-primary/60" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Assess & Route */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="order-2 lg:order-1 relative aspect-square lg:aspect-auto lg:h-[400px] rounded-3xl bg-secondary/50 border border-border flex items-center justify-center p-8 overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--purple-500)/0.05),transparent_70%)] pointer-events-none"></div>
|
||||
{/* Abstract decorative element representing routing */}
|
||||
<div className="flex items-center gap-4 relative z-10">
|
||||
<div className="w-16 h-16 rounded-xl bg-card border border-border flex items-center justify-center shadow-lg">
|
||||
<ShieldAlert className="w-8 h-8 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="w-12 h-[2px] bg-border relative">
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-purple-500"></div>
|
||||
</div>
|
||||
<div className="w-20 h-20 rounded-xl bg-purple-500/10 border border-purple-500/30 flex items-center justify-center shadow-[0_0_30px_-5px_hsl(var(--purple-500)/0.2)]">
|
||||
<BrainCircuit className="w-10 h-10 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="order-1 lg:order-2"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full bg-purple-500/10 border border-purple-500/20 flex items-center justify-center mb-8">
|
||||
<BrainCircuit className="w-8 h-8 text-purple-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Assess and route the hard parts
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
When a fix needs specialized work, such as post-quantum cryptographic remediation, Korrith assesses and routes that subset while you keep the client relationship and the assurance layer. You stay the trusted partner; the specialist work happens behind you.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What You Walk Away With (Final CTA) */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-card border border-border p-12 md:p-16 rounded-3xl relative overflow-hidden flex flex-col items-center"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.1),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<div className="w-16 h-16 rounded-2xl bg-primary/20 flex items-center justify-center mb-8 relative z-10 border border-primary/30">
|
||||
<Briefcase className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance relative z-10">
|
||||
What you walk away with
|
||||
</h2>
|
||||
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-10 max-w-2xl relative z-10">
|
||||
A new, defensible, recurring service line, delivered under your relationship, that makes your clients safer and stickier.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 relative z-10 w-full sm:w-auto">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="w-full sm:w-auto h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/contact">
|
||||
Become an agency partner
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { BrainCircuit, Search, ListOrdered, MessageSquare as MessageSquareText, History, ShieldAlert, ArrowRight, Sparkles } from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
export default function ArgusDetailPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Argus | The Reasoning Engine Behind Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Argus reasons over your real findings, connects them into attack paths, prioritizes by genuine risk, and explains every conclusion. Grounded in evidence, always."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[85vh] flex flex-col justify-center pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--accent-purple)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--primary)/0.1),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<BrainCircuit className="w-4 h-4 text-accent-purple" />
|
||||
The Intelligence Engine
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 leading-[1.1]">
|
||||
Argus reasons.<br />It does not guess.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-12">
|
||||
Argus is the security intelligence inside Korrith. It reads your real findings, connects them into attack paths, prioritizes by genuine risk, and explains every conclusion in language a board and an engineer can both follow. It is the difference between a tool that produces data and a tool that produces decisions.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Capabilities Section - Alternating Layout */}
|
||||
<section className="py-32 bg-background relative">
|
||||
{/* Decorative vertical line */}
|
||||
<div className="hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-border/40 -translate-x-1/2"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-32">
|
||||
|
||||
{/* 1. Grounded in evidence */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-24 items-center"
|
||||
>
|
||||
<div className="lg:pr-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary/80 flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<Search className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
Grounded in evidence
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Every insight Argus produces is tied to a specific, verifiable finding. This is enforced, not encouraged: if Argus cannot trace a statement back to a real finding, it does not make the statement.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
You can click any insight and land on the exact evidence behind it. That is what makes Argus safe to trust, and it is the opposite of a black box that asks you to take its word.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
{/* Abstract Visual */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-sm">
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 bg-card rounded-xl border border-border p-3 flex items-center gap-3 transform translate-x-4">
|
||||
<div className="w-2 h-2 rounded-full bg-primary animate-pulse"></div>
|
||||
<div className="h-2 bg-muted rounded w-24"></div>
|
||||
</div>
|
||||
<div className="h-12 bg-card rounded-xl border border-primary/40 shadow-[0_0_15px_-3px_hsl(var(--primary)/0.2)] p-3 flex items-center gap-3 relative z-10">
|
||||
<Search className="w-4 h-4 text-primary" />
|
||||
<div className="h-2 bg-foreground/80 rounded w-48"></div>
|
||||
</div>
|
||||
<div className="h-12 bg-card rounded-xl border border-border p-3 flex items-center gap-3 transform -translate-x-4">
|
||||
<div className="w-2 h-2 rounded-full bg-muted"></div>
|
||||
<div className="h-2 bg-muted rounded w-32"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Connecting lines */}
|
||||
<svg className="absolute top-0 left-0 w-full h-full -z-10" style={{ pointerEvents: 'none' }}>
|
||||
<path d="M40 24 L40 64" stroke="hsl(var(--border))" strokeWidth="2" strokeDasharray="4 4" fill="none" />
|
||||
<path d="M40 88 L40 128" stroke="hsl(var(--border))" strokeWidth="2" strokeDasharray="4 4" fill="none" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 2. Reasoned prioritization */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-24 items-center"
|
||||
>
|
||||
<div className="order-2 lg:order-1 relative h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--accent-purple)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
{/* Abstract Visual */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-[280px] flex items-end gap-3 h-48">
|
||||
<div className="w-full bg-card border border-border rounded-t-xl h-[30%]"></div>
|
||||
<div className="w-full bg-card border border-border rounded-t-xl h-[50%]"></div>
|
||||
<div className="w-full bg-primary/20 border-2 border-primary rounded-t-xl h-[100%] shadow-[0_0_30px_-5px_hsl(var(--primary)/0.3)] relative flex justify-center">
|
||||
<div className="absolute -top-8 bg-background border border-border rounded-lg px-2 py-1 text-xs font-mono text-primary flex items-center gap-1 shadow-sm">
|
||||
Priority 1
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-card border border-border rounded-t-xl h-[70%]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="order-1 lg:order-2 lg:pl-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary/80 flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<ListOrdered className="w-8 h-8 text-accent-purple" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
Reasoned prioritization, not raw severity
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
A severity score tells you something is bad in the abstract. Argus tells you what matters for you. It looks at which exposures connect into a real attack path, which an adversary would reach first, and which single fix would break the most paths at once.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
The result is a short, ordered list where the top item is genuinely the most important thing you can do today, not just the highest number in a table.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 3. It explains the why */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-24 items-center"
|
||||
>
|
||||
<div className="lg:pr-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary/80 flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<MessageSquareText className="w-8 h-8 text-emerald-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
It explains the why
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Argus writes the reasoning alongside the finding, in two registers: a plain-language version a non-technical stakeholder can act on, and a precise version an engineer can implement against.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
Findings stop being a backlog to triage and become decisions you can make.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden group p-8 flex flex-col justify-center gap-6">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--emerald)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="bg-card border border-border rounded-xl p-5 shadow-sm relative z-10 transform -rotate-1 hover:rotate-0 transition-transform">
|
||||
<div className="text-xs font-semibold tracking-wider text-muted-foreground uppercase mb-2">Executive Summary</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-2 bg-foreground/70 rounded w-full"></div>
|
||||
<div className="h-2 bg-foreground/70 rounded w-5/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0d1117] border border-border/80 rounded-xl p-5 shadow-lg relative z-10 transform translate-x-4 rotate-1 hover:rotate-0 transition-transform">
|
||||
<div className="text-xs font-mono text-emerald-400/70 mb-2">// Engineering Context</div>
|
||||
<div className="space-y-2 font-mono">
|
||||
<div className="h-2 bg-emerald-400/40 rounded w-3/4"></div>
|
||||
<div className="h-2 bg-emerald-400/40 rounded w-full"></div>
|
||||
<div className="h-2 bg-emerald-400/40 rounded w-1/2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 4. It reasons over time */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-24 items-center"
|
||||
>
|
||||
<div className="order-2 lg:order-1 relative h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden group flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--blue)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
{/* Abstract Timeline Visual */}
|
||||
<div className="relative w-64 h-64">
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 40, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute inset-0 border border-border rounded-full"
|
||||
></motion.div>
|
||||
<motion.div
|
||||
animate={{ rotate: -360 }}
|
||||
transition={{ duration: 30, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute inset-4 border border-border/60 rounded-full border-dashed"
|
||||
></motion.div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-16 h-16 bg-card border border-primary/30 rounded-full flex items-center justify-center shadow-[0_0_20px_-5px_hsl(var(--primary)/0.3)] z-10">
|
||||
<History className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Timeline nodes */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 bg-background border-2 border-muted-foreground rounded-full"></div>
|
||||
<div className="absolute bottom-1/4 right-0 translate-x-1/2 w-4 h-4 bg-background border-2 border-primary rounded-full shadow-[0_0_10px_hsl(var(--primary)/0.5)]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="order-1 lg:order-2 lg:pl-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary/80 flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<History className="w-8 h-8 text-blue-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
It reasons over time
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Argus does not just see today. It reasons longitudinally: what regressed, what is aging, how fast you are closing exposure, and where your trajectory is heading.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
That turns a point-in-time scan into a sense of whether you are getting safer or quietly slipping.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 5. It respects boundaries */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-24 items-center"
|
||||
>
|
||||
<div className="lg:pr-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary/80 flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<ShieldAlert className="w-8 h-8 text-rose-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
It respects boundaries
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Argus reasons within strict client boundaries and never crosses them, and it is constrained to reason only over real findings.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
It never invents risk, and unverified results are shown as unverified. Intelligence is useless if it hallucinates; Argus operates on a strict foundation of ground truth.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden group flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--rose)/0.05),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative w-full max-w-[280px]">
|
||||
{/* Shield abstract */}
|
||||
<div className="absolute inset-0 bg-rose-500/5 blur-2xl rounded-full"></div>
|
||||
<div className="bg-card border border-border rounded-2xl p-6 relative z-10 flex flex-col items-center shadow-lg transform group-hover:scale-105 transition-transform duration-500">
|
||||
<div className="w-12 h-12 rounded-full bg-rose-500/10 flex items-center justify-center mb-4">
|
||||
<div className="w-4 h-4 bg-rose-500 rounded-sm"></div>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded w-3/4 mb-3"></div>
|
||||
<div className="h-2 bg-muted rounded w-1/2"></div>
|
||||
<div className="mt-6 pt-4 border-t border-border w-full flex justify-center">
|
||||
<div className="text-xs font-medium text-muted-foreground flex items-center gap-1.5">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-rose-500"></div>
|
||||
Boundary enforced
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 6. What this gives you (Summary Bento Box) */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="bg-card border border-primary/20 rounded-3xl p-10 md:p-16 relative overflow-hidden text-center shadow-2xl"
|
||||
>
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-primary/10 rounded-full blur-[100px] -z-10 pointer-events-none"></div>
|
||||
|
||||
<div className="w-20 h-20 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-8 border border-primary/20">
|
||||
<Sparkles className="w-10 h-10 text-primary" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-8">
|
||||
What this gives you
|
||||
</h2>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mx-auto">
|
||||
A security analyst that works at machine speed, shows its evidence, prioritizes by real risk, explains itself, and never makes anything up.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-secondary/30 border-t border-border/50 text-center">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
Intelligence layered on deep visibility
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10">
|
||||
Argus relies on the Korrith sensor grid to gather the ground truth it reasons over.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/platform/argus-grid">
|
||||
See the Argus Grid
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Network,
|
||||
Layers,
|
||||
Globe,
|
||||
Server,
|
||||
Activity,
|
||||
ArrowRight,
|
||||
GitMerge,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
export default function ArgusGridPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>The Argus Grid | How Korrith Works</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Three layers of depth, external surface, host-internal posture, and authorized active validation, reasoned together by Argus into one prioritized story."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[75vh] flex flex-col justify-center pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.12),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--primary)/0.05),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<Network className="w-4 h-4 text-primary" />
|
||||
The Argus Grid
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 leading-[1.1]">
|
||||
Three layers of depth. One reasoned view.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-12">
|
||||
The Argus Grid is how Korrith sees you: from the outside in, the inside out, and then, with your authorization, actively proving what matters. Three layers of assessment, reasoned together by Argus so you get one prioritized story instead of three disconnected alert streams.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why a Grid Section */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-center">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="lg:col-span-5"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8">
|
||||
<Layers className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
Why a grid, and not a scanner
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="lg:col-span-7"
|
||||
>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-6">
|
||||
A scanner gives you a flat list. An adversary does not think in lists. They think in paths: what is exposed, what that exposure connects to, and how far they can get.
|
||||
</p>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed">
|
||||
The Argus Grid is built to mirror that. Each layer adds depth, and Argus reasons across all of them to reconstruct the paths an attacker would actually walk, then tells you which fix breaks the most of them at once.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Three Layers Section (Vertical Stacking / Timeline) */}
|
||||
<section className="py-24 bg-secondary/20 relative overflow-hidden">
|
||||
{/* Vertical connecting line for large screens */}
|
||||
<div className="hidden lg:block absolute left-1/2 top-32 bottom-32 w-px bg-border/50 -translate-x-1/2"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="space-y-24 relative z-10"
|
||||
>
|
||||
{/* Layer 1 */}
|
||||
<motion.div variants={fadeInUp} className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-center">
|
||||
<div className="order-2 lg:order-1 lg:text-right">
|
||||
<div className="inline-flex items-center justify-center px-3 py-1 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-sm font-bold tracking-wide mb-6">
|
||||
LAYER 1
|
||||
</div>
|
||||
<h3 className="text-3xl font-bold text-foreground mb-6">External attack surface</h3>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
What an adversary sees first, with nothing installed. The moment you verify a domain, Korrith assesses your internet-facing surface: security headers, TLS and certificate posture, email authentication, DNS hygiene, exposed services and ports, content-management fingerprinting and outdated components, and third-party script integrity, all weighted by live exploitability intelligence.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
This layer is live the instant you onboard, because it needs nothing from inside your perimeter.
|
||||
</p>
|
||||
</div>
|
||||
<div className="order-1 lg:order-2 flex justify-start lg:justify-center">
|
||||
<div className="w-24 h-24 lg:w-32 lg:h-32 rounded-3xl bg-card border border-border shadow-xl flex items-center justify-center relative group">
|
||||
<div className="absolute inset-0 bg-blue-500/10 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<Globe className="w-12 h-12 lg:w-16 lg:h-16 text-blue-400 relative z-10" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Layer 2 */}
|
||||
<motion.div variants={fadeInUp} className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-center">
|
||||
<div className="flex justify-start lg:justify-center">
|
||||
<div className="w-24 h-24 lg:w-32 lg:h-32 rounded-3xl bg-card border border-border shadow-xl flex items-center justify-center relative group">
|
||||
<div className="absolute inset-0 bg-purple-500/10 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<Server className="w-12 h-12 lg:w-16 lg:h-16 text-purple-400 relative z-10" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="inline-flex items-center justify-center px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-sm font-bold tracking-wide mb-6">
|
||||
LAYER 2
|
||||
</div>
|
||||
<h3 className="text-3xl font-bold text-foreground mb-6">Host-internal depth</h3>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
What an attacker would find once past the perimeter. A signed, read-only, least-privilege agent reports your host's real posture as cryptographically signed evidence: software inventory and known vulnerabilities, exposed secrets, configuration drift against recognized benchmarks, identity and access posture with file-integrity monitoring, cryptographic inventory and quantum exposure, operating-system patch level and end-of-life status, and host malware indicators.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
For sites where a server agent is not appropriate, a read-only, outbound-only plugin provides the same signed-evidence posture. The agent installs only under explicit, in-product authorization, and it never modifies your system or exfiltrates data.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Layer 3 */}
|
||||
<motion.div variants={fadeInUp} className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-center">
|
||||
<div className="order-2 lg:order-1 lg:text-right">
|
||||
<div className="inline-flex items-center justify-center px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm font-bold tracking-wide mb-6">
|
||||
LAYER 3
|
||||
</div>
|
||||
<h3 className="text-3xl font-bold text-foreground mb-6">Active validation</h3>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Proof, not theory. Under cryptographically signed scope, Korrith actively confirms which exposures are genuinely reachable and exploitable, turning a detected finding into a confirmed one. It starts with non-destructive reachability checks and, with explicit authorization, extends to controlled, lockout-safe validation of weak credentials and broken access controls.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
Every active test runs inside a signed authorization, an approved window, and a live kill-switch. The engine cannot act outside the scope you grant.
|
||||
</p>
|
||||
</div>
|
||||
<div className="order-1 lg:order-2 flex justify-start lg:justify-center">
|
||||
<div className="w-24 h-24 lg:w-32 lg:h-32 rounded-3xl bg-card border border-border shadow-xl flex items-center justify-center relative group">
|
||||
<div className="absolute inset-0 bg-emerald-500/10 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<Activity className="w-12 h-12 lg:w-16 lg:h-16 text-emerald-400 relative z-10" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Argus Ties It Together */}
|
||||
<section className="py-24 bg-background border-t border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
|
||||
{/* Visual connecting layers */}
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="bg-card border border-border/60 rounded-3xl p-8 relative overflow-hidden flex flex-col items-center justify-center min-h-[400px]"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.05),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<div className="flex flex-col gap-6 w-full max-w-sm relative z-10">
|
||||
{/* 3 Input nodes */}
|
||||
<div className="flex justify-between relative">
|
||||
<motion.div
|
||||
animate={{ y: [0, -5, 0] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut", delay: 0 }}
|
||||
className="w-16 h-16 rounded-xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400"
|
||||
>
|
||||
<Globe className="w-6 h-6" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ y: [0, -5, 0] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut", delay: 1 }}
|
||||
className="w-16 h-16 rounded-xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400"
|
||||
>
|
||||
<Server className="w-6 h-6" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ y: [0, -5, 0] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut", delay: 2 }}
|
||||
className="w-16 h-16 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400"
|
||||
>
|
||||
<Activity className="w-6 h-6" />
|
||||
</motion.div>
|
||||
|
||||
{/* Lines converging */}
|
||||
<svg className="absolute top-16 left-0 w-full h-24" preserveAspectRatio="none">
|
||||
<path d="M32 0 L160 96" stroke="hsl(var(--border))" strokeWidth="2" fill="none" strokeDasharray="4 4" />
|
||||
<path d="M160 0 L160 96" stroke="hsl(var(--border))" strokeWidth="2" fill="none" strokeDasharray="4 4" />
|
||||
<path d="M288 0 L160 96" stroke="hsl(var(--border))" strokeWidth="2" fill="none" strokeDasharray="4 4" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Combined node */}
|
||||
<div className="flex justify-center mt-20 relative">
|
||||
<motion.div
|
||||
animate={{ boxShadow: ["0 0 0 0 rgba(0,0,0,0)", "0 0 20px 2px hsl(var(--primary)/0.3)", "0 0 0 0 rgba(0,0,0,0)"] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
className="w-20 h-20 rounded-xl bg-primary text-primary-foreground flex items-center justify-center shadow-lg relative z-10"
|
||||
>
|
||||
<GitMerge className="w-10 h-10" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div {...fadeInUp}>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
Argus ties it together
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-6">
|
||||
A header weakness on Layer 1, a host misconfiguration on Layer 2, and a validated foothold on Layer 3 are not three separate problems.
|
||||
</p>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed">
|
||||
Argus reasons across all three to show the path they form, prioritizes by genuine risk, and explains the result in plain language. You get one fix list, ordered by what actually reduces your exposure most.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Note Section */}
|
||||
<section className="py-12 bg-background border-t border-border/50">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="flex gap-4 p-6 rounded-2xl bg-secondary/50 border border-border/60"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<Info className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<p className="text-base text-secondary-foreground leading-relaxed">
|
||||
<strong>Note:</strong> all three layers and the host agent are built and rolling out to the beta. Client-facing active testing on Layer 3 is gated behind active-testing terms reviewed by counsel.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-card/20 border-t border-border/50 text-center">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp}>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
See the grid in action
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10">
|
||||
Discover how comprehensive visibility changes the way you approach security.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="w-full sm:w-auto h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/platform/argus">
|
||||
See Argus
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full sm:w-auto h-14 px-8 text-base font-semibold transition-colors"
|
||||
>
|
||||
<Link to="/assessment">
|
||||
Get a free assessment
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
|
||||
export default function ArgusPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet><title>Argus | Korrith</title></Helmet>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-foreground">Argus</h1>
|
||||
<p className="mt-4 text-muted-foreground text-lg">Content coming soon in Phase 2.</p>
|
||||
</div>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
ShieldCheck,
|
||||
Lock,
|
||||
Mail,
|
||||
Globe,
|
||||
Server,
|
||||
FileCode,
|
||||
Code,
|
||||
ArrowRight,
|
||||
ShieldAlert,
|
||||
Search,
|
||||
CheckCircle2
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { toast } from 'sonner';
|
||||
import apiServerClient from '@/lib/apiServerClient.js';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.1 }
|
||||
};
|
||||
|
||||
const initialFormState = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
industry: '',
|
||||
websites: '',
|
||||
applications: '',
|
||||
systems: ''
|
||||
};
|
||||
|
||||
export default function AssessmentPage() {
|
||||
const [formData, setFormData] = useState(initialFormState);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
|
||||
const handleScrollToForm = () => {
|
||||
document.getElementById('assessment-form')?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const handleResetRequest = () => {
|
||||
setFormData(initialFormState);
|
||||
setIsSuccess(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Client-side validation
|
||||
if (!formData.industry) {
|
||||
toast.error('Please select an industry from the dropdown');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await apiServerClient.fetch('/assessment/submit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
firstName: formData.firstName,
|
||||
lastName: formData.lastName,
|
||||
email: formData.email,
|
||||
industry: formData.industry,
|
||||
websites: formData.websites,
|
||||
applications: formData.applications,
|
||||
systems: formData.systems
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData = { error: 'Request failed' };
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch (parseErr) {
|
||||
console.error('Could not parse error response:', parseErr);
|
||||
}
|
||||
throw new Error(errorData.error || 'Failed to submit assessment request');
|
||||
}
|
||||
|
||||
// Parse success response
|
||||
const data = await response.json();
|
||||
console.log('Assessment submitted successfully:', data);
|
||||
|
||||
setIsSuccess(true);
|
||||
toast.success('Your assessment request has been submitted successfully');
|
||||
|
||||
// Reset form fields after successful submission
|
||||
setFormData(initialFormState);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Submission error:', error);
|
||||
toast.error(error.message || 'An unexpected error occurred. Please try again');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const assessmentChecks = [
|
||||
{ icon: ShieldCheck, title: "Security headers", desc: "How your site instructs browsers to protect users." },
|
||||
{ icon: Lock, title: "TLS & Certificates", desc: "TLS versions, cipher strength, and certificate validity and expiry." },
|
||||
{ icon: Mail, title: "Email authentication", desc: "SPF, DKIM, DMARC, and MTA-STS, the records that stop your domain being spoofed." },
|
||||
{ icon: Globe, title: "DNS hygiene", desc: "Including DNSSEC and CAA configurations." },
|
||||
{ icon: Server, title: "Internet-exposed services", desc: "Mapping open ports and exposed administrative interfaces." },
|
||||
{ icon: FileCode, title: "Content management", desc: "Fingerprinting and detecting outdated public components." },
|
||||
{ icon: Code, title: "Third-party script integrity", desc: "Validating the security of external dependencies loaded by your site." },
|
||||
];
|
||||
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>{`Get a Free External Assessment | Korrith`}</title>
|
||||
<meta name="description" content="Verify a domain and Korrith assesses your external attack surface with nothing to install. See what an adversary sees, prioritized and explained." />
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[90dvh] flex items-center pt-24 pb-16 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.12),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--accent-purple)/0.1),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<span className="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
|
||||
Assessment Engine Beta
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 leading-[1.1]">
|
||||
See your real exposure. <br className="hidden md:block" />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent-purple">Start free.</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-12">
|
||||
Verify a domain and Korrith assesses your external attack surface, with nothing to install and nothing to configure, then shows you what an adversary sees, prioritized and explained in plain language by Argus.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={handleScrollToForm}
|
||||
className="h-14 px-8 text-lg font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
Get a free assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why Start With The Outside */}
|
||||
<section className="py-24 bg-card/20 relative">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="bg-card border border-border/60 rounded-3xl p-8 md:p-12 shadow-lg relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/3"></div>
|
||||
|
||||
<div className="relative z-10 max-w-3xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-foreground">Why start with the outside</h2>
|
||||
<p className="text-lg md:text-xl text-muted-foreground leading-relaxed">
|
||||
The outside is where every attacker starts, and it is the part of your security you can assess with zero friction and zero risk. No agent, no access, no change to your systems. You verify that you control the domain, and Korrith looks at it the way the internet can already see it.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What the free assessment looks at */}
|
||||
<section className="py-24 border-y border-border/50 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp} className="mb-16 max-w-3xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-foreground">What the free assessment looks at</h2>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
We analyze your public footprint comprehensively to build a true picture of your external posture.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16"
|
||||
>
|
||||
{assessmentChecks.map((item, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={fadeInUp}
|
||||
className="bg-secondary/40 border border-border/40 rounded-2xl p-6 hover:bg-secondary/60 transition-colors flex flex-col h-full"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary mb-4">
|
||||
<item.icon size={20} />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">{item.title}</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">{item.desc}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="bg-primary/5 border border-primary/20 rounded-xl p-6 md:p-8 text-center max-w-4xl mx-auto"
|
||||
>
|
||||
<p className="text-lg md:text-xl text-foreground leading-relaxed font-medium">
|
||||
Each finding is weighted with known-exploited and likely-exploited intelligence, so the priorities reflect what attackers are actually using right now, not a raw severity table.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Form Section */}
|
||||
<section id="assessment-form" className="py-24 border-t border-border/50 relative overflow-hidden bg-background">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.03),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="bg-card border border-border/60 rounded-3xl shadow-xl overflow-hidden mb-12"
|
||||
>
|
||||
{isSuccess ? (
|
||||
<div className="p-8 md:p-16 text-center">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-primary/10 text-primary mb-6"
|
||||
>
|
||||
<CheckCircle2 className="w-10 h-10" />
|
||||
</motion.div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">Thank you</h2>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-xl mx-auto">
|
||||
Your assessment request has been submitted successfully. We will review your details and be in touch regarding early access to the beta.
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleResetRequest}
|
||||
variant="outline"
|
||||
className="h-12 px-8 text-base border-border hover:bg-secondary"
|
||||
>
|
||||
Submit another request
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 md:p-12">
|
||||
<div className="text-center mb-10">
|
||||
<Search className="w-12 h-12 text-primary mx-auto mb-4" />
|
||||
<h2 className="text-3xl font-bold text-foreground mb-3">Request your assessment</h2>
|
||||
<p className="text-muted-foreground text-lg">Provide your details to get started.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-8 max-w-3xl mx-auto">
|
||||
{/* User Details */}
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName" className="text-foreground">First Name <span className="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
type="text"
|
||||
placeholder="Jane"
|
||||
required
|
||||
value={formData.firstName}
|
||||
onChange={(e) => setFormData({...formData, firstName: e.target.value})}
|
||||
className="bg-background border-border h-12 px-4 text-base text-foreground placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName" className="text-foreground">Last Name <span className="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
type="text"
|
||||
placeholder="Doe"
|
||||
required
|
||||
value={formData.lastName}
|
||||
onChange={(e) => setFormData({...formData, lastName: e.target.value})}
|
||||
className="bg-background border-border h-12 px-4 text-base text-foreground placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-foreground">Email <span className="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="name@company.com"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
||||
className="bg-background border-border h-12 px-4 text-base text-foreground placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="industry" className="text-foreground">Industry <span className="text-destructive">*</span></Label>
|
||||
<Select
|
||||
value={formData.industry}
|
||||
onValueChange={(val) => setFormData({...formData, industry: val})}
|
||||
>
|
||||
<SelectTrigger id="industry" className="bg-background border-border h-12 px-4 text-base text-foreground">
|
||||
<SelectValue placeholder="Select Industry" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Healthcare">Healthcare</SelectItem>
|
||||
<SelectItem value="Construction">Construction</SelectItem>
|
||||
<SelectItem value="Manufacturing">Manufacturing</SelectItem>
|
||||
<SelectItem value="Government">Government</SelectItem>
|
||||
<SelectItem value="Technology">Technology</SelectItem>
|
||||
<SelectItem value="Finance">Finance</SelectItem>
|
||||
<SelectItem value="Other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Assessment Targets - Text Inputs */}
|
||||
<div className="space-y-6 pt-6 border-t border-border/50">
|
||||
<div className="space-y-2 mb-4">
|
||||
<Label className="text-xl font-bold text-foreground">Target Information <span className="text-muted-foreground text-base font-normal ml-2">(Optional)</span></Label>
|
||||
<p className="text-muted-foreground text-sm">Please provide target details for any relevant categories you wish to assess.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="websites" className="text-foreground">Websites</Label>
|
||||
<Input
|
||||
id="websites"
|
||||
type="text"
|
||||
placeholder="e.g., example.com"
|
||||
value={formData.websites}
|
||||
onChange={(e) => setFormData({...formData, websites: e.target.value})}
|
||||
className="bg-background border-border h-12 px-4 text-base text-foreground placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="applications" className="text-foreground">Applications</Label>
|
||||
<Input
|
||||
id="applications"
|
||||
type="text"
|
||||
placeholder="e.g., app.example.com"
|
||||
value={formData.applications}
|
||||
onChange={(e) => setFormData({...formData, applications: e.target.value})}
|
||||
className="bg-background border-border h-12 px-4 text-base text-foreground placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="systems" className="text-foreground">Systems</Label>
|
||||
<Input
|
||||
id="systems"
|
||||
type="text"
|
||||
placeholder="e.g., api.example.com"
|
||||
value={formData.systems}
|
||||
onChange={(e) => setFormData({...formData, systems: e.target.value})}
|
||||
className="bg-background border-border h-12 px-4 text-base text-foreground placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full h-14 text-lg font-semibold bg-primary text-primary-foreground hover:bg-primary/90 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg active:scale-[0.98]"
|
||||
>
|
||||
{isSubmitting ? "Processing..." : "Submit Assessment Request"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.div {...fadeInUp} className="text-center max-w-2xl mx-auto">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-secondary text-muted-foreground mb-6">
|
||||
<ShieldAlert size={24} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-foreground mb-4">What it will not do</h3>
|
||||
<p className="text-muted-foreground leading-relaxed mb-6">
|
||||
Korrith assesses only what you verify and authorize. The free assessment is external and non-intrusive. It does not log in, it does not attack, and it does not touch anything you have not authorized.
|
||||
</p>
|
||||
<div className="inline-block bg-secondary/50 border border-border/50 rounded-lg px-4 py-3 text-sm text-muted-foreground">
|
||||
<span className="font-semibold text-foreground mr-1">Note:</span>
|
||||
the assessment engine is in private testing, and early access is rolling out to the beta.
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
ArrowRight,
|
||||
BookOpen,
|
||||
Terminal,
|
||||
Shield,
|
||||
FileText,
|
||||
Cpu,
|
||||
Clock,
|
||||
Sparkles,
|
||||
Lock,
|
||||
Search
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
import { Card, CardContent } from '@/components/ui/card.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.1 }
|
||||
};
|
||||
|
||||
const topics = [
|
||||
{
|
||||
icon: Sparkles,
|
||||
text: "The modern adversary: how AI and the dark-web economy changed who you are defending against."
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
text: "Why a finding is worthless until it is validated, and how to tell the difference."
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
text: "Quantum readiness in practical terms, without the hype."
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
text: "How active security testing can be made genuinely safe."
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
text: "What audit-grade evidence really means, and why a checklist is not it."
|
||||
}
|
||||
];
|
||||
|
||||
const upcomingArticles = [
|
||||
{
|
||||
title: "Your credentials are probably already for sale.",
|
||||
subtitle: "What stealer logs and breach dumps mean for your real exposure, and how to find out."
|
||||
},
|
||||
{
|
||||
title: "Detected is not exploitable.",
|
||||
subtitle: "Why validation is the step that separates real risk from noise."
|
||||
},
|
||||
{
|
||||
title: "Defense at machine speed.",
|
||||
subtitle: "What it looks like to reason about security as fast as attackers now move."
|
||||
},
|
||||
{
|
||||
title: "Authorization as a hard boundary.",
|
||||
subtitle: "How a cryptographic gate lets you run offensive testing without becoming a liability."
|
||||
},
|
||||
{
|
||||
title: "Harvest now, decrypt later.",
|
||||
subtitle: "A plain-language guide to quantum risk for people who are not cryptographers."
|
||||
}
|
||||
];
|
||||
|
||||
export default function BlogPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Korrith Blog | Security Assurance, the Threat Landscape, and Proof</title>
|
||||
<meta name="description" content="Practical writing on AI-accelerated attackers, the dark-web economy, quantum readiness, and what it really takes to assess, validate, and prove your security." />
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[70vh] flex flex-col justify-center pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.1),transparent_60%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--accent-purple)/0.08),transparent_60%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<BookOpen className="w-4 h-4 text-primary" />
|
||||
Korrith Research & Analysis
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 leading-[1.1]">
|
||||
Notes on proving security.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-muted-foreground leading-relaxed max-w-2xl mb-12">
|
||||
Practical, opinionated writing on the modern threat landscape and what it actually takes to assess, validate, and prove your security, from the team building Korrith. No vendor fluff, no fear-selling, just useful thinking you can apply.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-korrith group"
|
||||
>
|
||||
<Link to="/assessment">
|
||||
Get a free assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured & Latest Posts (Placeholder Layout) */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp} className="mb-12">
|
||||
<h2 className="text-3xl font-bold text-foreground">Featured Analysis</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Featured Post Card (1 large span) */}
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="bg-card border border-border/60 rounded-3xl overflow-hidden shadow-lg hover:shadow-primary/5 transition-korrith group mb-16 relative"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/3"></div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 relative z-10">
|
||||
<div className="p-8 md:p-12 flex flex-col justify-center">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<span className="text-sm font-medium text-primary tracking-wide uppercase">Research</span>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-border"></span>
|
||||
<span className="text-sm text-muted-foreground flex items-center gap-1.5">
|
||||
<Clock className="w-4 h-4" /> 8 min read
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-3xl md:text-4xl font-bold text-card-foreground mb-4 leading-tight group-hover:text-primary transition-colors">
|
||||
The End of the Scanner Era: Why Detection Without Validation is Dead
|
||||
</h3>
|
||||
<p className="text-lg text-muted-foreground mb-8 line-clamp-3">
|
||||
As external attack surfaces expand and vulnerability disclosures accelerate, relying on raw scanner output has become a liability. Security teams are drowning in noise. It's time for a fundamental shift toward evidence-backed validation.
|
||||
</p>
|
||||
<div className="mt-auto">
|
||||
<Link to="#" className="inline-flex items-center text-primary font-medium hover:text-primary/80 transition-colors">
|
||||
Read featured article <ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-secondary/40 lg:border-l border-border/50 flex items-center justify-center p-12 min-h-[300px]">
|
||||
{/* Abstract graphic placeholder */}
|
||||
<div className="relative w-full max-w-sm aspect-square flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-primary/10 rounded-full blur-2xl animate-pulse"></div>
|
||||
<Terminal className="w-32 h-32 text-primary/40 relative z-10" strokeWidth={1} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div {...fadeInUp} className="mb-10">
|
||||
<h2 className="text-2xl font-bold text-foreground">Latest Posts</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Latest Posts Grid (2 columns to avoid the 3-card generic AI pattern) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{[
|
||||
{
|
||||
category: "Threat Intel",
|
||||
title: "How to actually read a CVE",
|
||||
excerpt: "Most organizations treat CVE severity scores as gospel. Here is why the base score is only half the story, and how to assess true environmental risk.",
|
||||
time: "5 min read"
|
||||
},
|
||||
{
|
||||
category: "Engineering",
|
||||
title: "Building an honest AI for security",
|
||||
excerpt: "The technical choices behind Korrith's Argus engine. How we constrained the model to prevent hallucination and ensure every claim is backed by cryptographically verifiable proof.",
|
||||
time: "10 min read"
|
||||
}
|
||||
].map((post, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
viewport={{ once: true }}
|
||||
className="bg-card border border-border/50 rounded-2xl p-8 hover:bg-secondary/30 transition-colors flex flex-col group cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="text-xs font-semibold text-accent-purple tracking-wide uppercase">{post.category}</span>
|
||||
<span className="w-1 h-1 rounded-full bg-border"></span>
|
||||
<span className="text-xs text-muted-foreground">{post.time}</span>
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-card-foreground mb-3 group-hover:text-primary transition-colors">
|
||||
{post.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6 line-clamp-2">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
<div className="mt-auto pt-4 border-t border-border/40">
|
||||
<span className="text-sm font-medium text-foreground group-hover:text-primary transition-colors flex items-center">
|
||||
Read post <ArrowRight className="ml-1 w-4 h-4 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What we write about Section */}
|
||||
<section className="py-24 bg-card/30 border-y border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16">
|
||||
<motion.div {...fadeInUp} className="lg:col-span-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-foreground sticky top-24">
|
||||
What we write about
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
We focus on the intersection of modern infrastructure, evolving threat models, and cryptographic proof.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="lg:col-span-8 space-y-6"
|
||||
>
|
||||
{topics.map((topic, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={fadeInUp}
|
||||
className="bg-background border border-border/60 rounded-xl p-6 flex gap-6 items-start"
|
||||
>
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-secondary/80 flex items-center justify-center text-primary mt-1">
|
||||
<topic.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<p className="text-lg text-foreground font-medium leading-relaxed pt-2">
|
||||
{topic.text}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Launch Articles (In Progress) Section */}
|
||||
<section className="py-24 bg-background relative overflow-hidden">
|
||||
<div className="absolute right-0 bottom-0 w-1/2 h-1/2 bg-[radial-gradient(ellipse_at_bottom_right,hsl(var(--primary)/0.05),transparent_60%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div {...fadeInUp} className="mb-16 max-w-2xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-foreground flex items-center gap-3">
|
||||
Launch articles
|
||||
<span className="text-sm font-normal px-3 py-1 bg-secondary text-secondary-foreground rounded-full border border-border">In progress</span>
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
We are currently drafting our foundational series. These deep-dives will establish the technical context for the Korrith platform.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="space-y-4"
|
||||
>
|
||||
{upcomingArticles.map((article, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={fadeInUp}
|
||||
className="group relative flex flex-col md:flex-row md:items-center gap-4 md:gap-8 p-6 md:p-8 rounded-2xl border border-border/40 bg-card/20 hover:bg-card/60 transition-colors"
|
||||
>
|
||||
<div className="hidden md:flex text-4xl font-bold text-border group-hover:text-primary/20 transition-colors font-mono">
|
||||
{(i + 1).toString().padStart(2, '0')}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">
|
||||
{article.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-base">
|
||||
{article.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 pt-4 md:pt-0">
|
||||
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-secondary text-muted-foreground">
|
||||
<Lock className="w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Building,
|
||||
ArrowRight,
|
||||
Eye,
|
||||
Crosshair,
|
||||
FileCheck,
|
||||
Terminal,
|
||||
ActivitySquare,
|
||||
Presentation
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
const howItChanges = [
|
||||
{
|
||||
icon: Eye,
|
||||
title: "See your true exposure.",
|
||||
description: "External first, the way an adversary maps you, with host-internal depth through a read-only agent when you are ready. Honestly scored, no fake-clean.",
|
||||
color: "text-blue-400"
|
||||
},
|
||||
{
|
||||
icon: Crosshair,
|
||||
title: "Fix what actually matters.",
|
||||
description: "Argus prioritizes by genuine risk and validation confirms what is real, so your team works the short list that reduces exposure most, not the long list that exhausts them.",
|
||||
color: "text-rose-400"
|
||||
},
|
||||
{
|
||||
icon: FileCheck,
|
||||
title: "Walk into audits with evidence.",
|
||||
description: "Validated findings and proof of resolution are the evidence auditors ask for. You export the truth instead of assembling a story.",
|
||||
color: "text-emerald-400"
|
||||
}
|
||||
];
|
||||
|
||||
export default function BusinessesPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Korrith for Businesses | Prove Your Security, Do Not Just Hope</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="For businesses that have to answer the security question to customers, auditors, and boards, Korrith assesses real exposure, validates what matters, and produces proof."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 md:pt-40 md:pb-32 overflow-hidden border-b border-border/50 min-h-[85vh] flex items-center">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,rgba(139,92,246,0.1),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<Building className="w-4 h-4 text-primary" />
|
||||
For Businesses
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
Prove your security. <br className="hidden md:block" />
|
||||
<span className="text-muted-foreground">Do not just hope for it.</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-10">
|
||||
Sooner or later someone, a customer, an auditor, a board, a regulator, asks you to demonstrate that you are secure. Korrith makes that a moment you are ready for: it assesses your real exposure, validates what an attacker could actually use, and produces the evidence you need to answer with confidence.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group">
|
||||
<Link to="/assessment">
|
||||
Get your assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Trap Section */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-8 text-balance">
|
||||
The trap most businesses are in
|
||||
</h2>
|
||||
<div className="p-8 md:p-10 rounded-3xl bg-secondary/30 border border-border/50 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-rose-500/5 rounded-full blur-3xl"></div>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed relative z-10">
|
||||
You have tools that generate findings, but no way to know which ones are real, no proof that the ones you fixed are gone, and no clean evidence when someone asks. Security feels like an unending backlog and an annual scramble. Attackers, meanwhile, are faster and better resourced than ever, buying access on the dark web and moving at machine speed.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How Korrith Changes That (Numbered List Layout) */}
|
||||
<section className="py-24 bg-secondary/10 border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="mb-16 md:mb-24"
|
||||
>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-6 text-balance">
|
||||
How Korrith changes that
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="flex flex-col gap-12 md:gap-20 max-w-4xl"
|
||||
>
|
||||
{howItChanges.map((item, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
variants={fadeInUp}
|
||||
className="group flex flex-col md:flex-row gap-6 md:gap-10 items-start"
|
||||
>
|
||||
<div className="text-6xl md:text-8xl font-black text-primary/10 tracking-tighter shrink-0 select-none group-hover:text-primary/20 transition-colors duration-500">
|
||||
0{idx + 1}
|
||||
</div>
|
||||
<div className="pt-2 md:pt-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<item.icon className={`w-6 h-6 ${item.color}`} />
|
||||
<h3 className="text-2xl font-semibold text-foreground">
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground leading-relaxed text-lg max-w-2xl">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* For the Whole Organization (Bento Grid) */}
|
||||
<section className="py-24 bg-background border-b border-border/50 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-6 text-balance">
|
||||
For the whole organization
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
||||
>
|
||||
{/* Bento Item 1: Engineers (Col span 2) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="md:col-span-2 bg-card border border-border p-8 rounded-3xl hover:bg-secondary/40 transition-colors"
|
||||
>
|
||||
<Terminal className="w-10 h-10 text-blue-400 mb-6" />
|
||||
<h3 className="text-xl font-medium text-foreground mb-2">Engineers</h3>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Your engineers get a short, reasoned, explained list instead of a noise dump.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Bento Item 2: Security Lead (Col span 1) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="md:col-span-1 bg-card border border-border p-8 rounded-3xl hover:bg-secondary/40 transition-colors flex flex-col"
|
||||
>
|
||||
<ActivitySquare className="w-10 h-10 text-purple-400 mb-6" />
|
||||
<h3 className="text-xl font-medium text-foreground mb-2">Security Lead</h3>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Your security lead gets a defensible, honest posture and a trajectory over time.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Bento Item 3: Executives (Col span 3) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="md:col-span-3 bg-secondary/30 border border-border p-8 md:p-12 rounded-3xl hover:bg-secondary/50 transition-colors flex flex-col md:flex-row gap-8 items-start md:items-center relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute right-0 bottom-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-[100px] pointer-events-none translate-x-1/3 translate-y-1/3"></div>
|
||||
|
||||
<div className="w-16 h-16 rounded-2xl bg-card border border-border flex items-center justify-center shrink-0 shadow-lg relative z-10">
|
||||
<Presentation className="w-8 h-8 text-emerald-400" />
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<h3 className="text-2xl font-semibold text-foreground mb-2">Executives & Leadership</h3>
|
||||
<p className="text-muted-foreground text-xl max-w-3xl">
|
||||
Your executives get something they can show a board, an auditor, or a customer without flinching.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-card border border-border p-12 md:p-16 rounded-3xl relative overflow-hidden flex flex-col items-center"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.1),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance relative z-10">
|
||||
Start with no commitment
|
||||
</h2>
|
||||
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-10 max-w-2xl relative z-10">
|
||||
Run a free external assessment on your own domain. No install, no risk, and a real look at what an adversary sees.
|
||||
</p>
|
||||
|
||||
<div className="relative z-10 w-full sm:w-auto">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="w-full sm:w-auto h-14 px-10 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/assessment">
|
||||
Get a free assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { ArrowRight, Mail, Calendar, CheckCircle2 } from 'lucide-react';
|
||||
|
||||
export default function ContactPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
message: ''
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate API call and save to local storage
|
||||
setTimeout(() => {
|
||||
localStorage.setItem('korrith_contact', JSON.stringify({
|
||||
...formData,
|
||||
timestamp: new Date().toISOString()
|
||||
}));
|
||||
setIsSubmitting(false);
|
||||
setIsSuccess(true);
|
||||
setFormData({ name: '', email: '', company: '', message: '' });
|
||||
}, 800);
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Contact Us | Korrith</title>
|
||||
<meta name="description" content="Reach out to the Korrith team to ask questions, learn about our development progress, or inquire about future beta testing." />
|
||||
</Helmet>
|
||||
|
||||
<section className="relative pt-32 pb-24 overflow-hidden">
|
||||
<div className="absolute inset-0 radial-glow-top pointer-events-none opacity-50"></div>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16">
|
||||
|
||||
{/* Left Column: Info */}
|
||||
<div className="flex flex-col justify-start">
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-foreground mb-6">
|
||||
Get in touch
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-12">
|
||||
We are actively building our post-quantum readiness platform and welcome inquiries. Reach out to ask questions, learn about our timeline, or discuss joining our upcoming beta.
|
||||
</p>
|
||||
|
||||
<div className="space-y-8 mt-auto hidden lg:block">
|
||||
<div className="flex items-start gap-4 p-6 rounded-2xl bg-card border border-border/50">
|
||||
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<Calendar className="text-primary" size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground mb-1">Beta access waitlist</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">Interested in testing our tools? Join our waitlist and we will keep you updated on beta availability.</p>
|
||||
<a href="/assessment" className="text-sm font-medium text-primary hover:text-primary/80 inline-flex items-center gap-1 transition-colors">
|
||||
Join the waitlist <ArrowRight size={14} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4 p-6 rounded-2xl bg-card border border-border/50">
|
||||
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<Mail className="text-primary" size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground mb-1">Direct email</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">Prefer standard email? Reach out to our small development team directly.</p>
|
||||
<a href="mailto:hello@korrith.com" className="text-sm font-medium text-foreground hover:text-primary transition-colors">
|
||||
hello@korrith.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Form */}
|
||||
<div className="bg-card border border-border/50 rounded-2xl p-6 md:p-8 shadow-xl">
|
||||
{isSuccess ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center py-12 space-y-4">
|
||||
<div className="h-16 w-16 bg-primary/10 rounded-full flex items-center justify-center text-primary mb-2">
|
||||
<CheckCircle2 size={32} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-foreground">Message sent</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Thank you for reaching out. A member of our team will review your message and reply as soon as possible.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setIsSuccess(false)}
|
||||
className="mt-6 px-6 py-2 bg-secondary text-secondary-foreground rounded-lg font-medium hover:bg-secondary/80 transition-colors"
|
||||
>
|
||||
Send another message
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="name" className="text-sm font-medium text-foreground">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-input border border-border rounded-lg px-4 py-3 text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-all"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-medium text-foreground">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-input border border-border rounded-lg px-4 py-3 text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-all"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="company" className="text-sm font-medium text-foreground flex justify-between">
|
||||
<span>Company</span>
|
||||
<span className="text-muted-foreground font-normal">Optional</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="company"
|
||||
name="company"
|
||||
value={formData.company}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-input border border-border rounded-lg px-4 py-3 text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-all"
|
||||
placeholder="Your organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="message" className="text-sm font-medium text-foreground">Message</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
rows={5}
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-input border border-border rounded-lg px-4 py-3 text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-all resize-none"
|
||||
placeholder="How can we help you?"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-primary text-primary-foreground font-semibold rounded-lg px-4 py-3 hover:bg-primary/90 hover:shadow-lg hover:-translate-y-0.5 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0"
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Send message'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet><title>Documentation | Korrith</title></Helmet>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-foreground">Documentation</h1>
|
||||
<p className="mt-4 text-muted-foreground text-lg">Content coming soon in Phase 2.</p>
|
||||
</div>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
BookOpen,
|
||||
Rocket,
|
||||
Activity,
|
||||
Server,
|
||||
ShieldAlert,
|
||||
FileCheck,
|
||||
Briefcase,
|
||||
ArrowRight,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.1 }
|
||||
};
|
||||
|
||||
const guides = [
|
||||
{
|
||||
icon: Rocket,
|
||||
title: "Getting started",
|
||||
description: "Verify a domain and run your first external assessment.",
|
||||
color: "text-emerald-400",
|
||||
bg: "bg-emerald-400/10 border-emerald-400/20"
|
||||
},
|
||||
{
|
||||
icon: Activity,
|
||||
title: "Understanding your results",
|
||||
description: "How scoring works, how Argus prioritizes, and how to read validation status.",
|
||||
color: "text-primary",
|
||||
bg: "bg-primary/10 border-primary/20"
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: "Host-internal assessment",
|
||||
description: "Authorizing access and installing the read-only, least-privilege agent or the WordPress plugin, and exactly what each reads and never does.",
|
||||
color: "text-accent-purple",
|
||||
bg: "bg-accent-purple/10 border-accent-purple/20"
|
||||
},
|
||||
{
|
||||
icon: ShieldAlert,
|
||||
title: "Authorization and safety",
|
||||
description: "How the signed authorization gate, scoping, and the kill-switch govern any active testing.",
|
||||
color: "text-amber-400",
|
||||
bg: "bg-amber-400/10 border-amber-400/20"
|
||||
},
|
||||
{
|
||||
icon: FileCheck,
|
||||
title: "Reports and evidence",
|
||||
description: "Generating, reading, and sharing audit-grade reports and proof of resolution.",
|
||||
color: "text-blue-400",
|
||||
bg: "bg-blue-400/10 border-blue-400/20"
|
||||
},
|
||||
{
|
||||
icon: Briefcase,
|
||||
title: "For partners",
|
||||
description: "Running multi-client assessment with strict per-client boundaries.",
|
||||
color: "text-rose-400",
|
||||
bg: "bg-rose-400/10 border-rose-400/20"
|
||||
}
|
||||
];
|
||||
|
||||
export default function DocumentationPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Documentation | Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Guides for running assessments, authorizing access, installing the read-only agent, reading reports, and understanding authorization and safety."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 md:pt-40 md:pb-32 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--accent-purple)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--primary)/0.1),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<BookOpen className="w-4 h-4 text-accent-purple" />
|
||||
Documentation
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
Everything you need to use Korrith well.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-10">
|
||||
Clear, practical documentation for getting value from Korrith quickly, whether you are running your first external assessment or authorizing deep host-internal validation.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" className="h-14 px-8 text-base font-semibold shadow-[0_0_20px_-5px_hsl(var(--accent-purple)/0.4)] hover:shadow-[0_0_30px_-5px_hsl(var(--accent-purple)/0.6)] transition-all bg-accent-purple hover:bg-accent-purple/90 text-white">
|
||||
<Link to="/resources/documentation#guides">Browse the docs</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Guides Section */}
|
||||
<section id="guides" className="py-24 bg-background scroll-mt-24">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">Guides</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl">
|
||||
Step-by-step instructions for configuring, operating, and reading outputs from the Korrith platform.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
|
||||
>
|
||||
{guides.map((guide, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
variants={fadeInUp}
|
||||
className="group relative flex flex-col h-full bg-card hover:bg-secondary/40 border border-border hover:border-border/80 rounded-3xl p-8 transition-all duration-300"
|
||||
>
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center border mb-6 ${guide.bg} group-hover:scale-110 transition-transform duration-300`}>
|
||||
<guide.icon className={`w-6 h-6 ${guide.color}`} />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold text-foreground mb-3 pr-8 group-hover:text-primary transition-colors">
|
||||
{guide.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground leading-relaxed flex-grow">
|
||||
{guide.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex items-center text-sm font-medium text-muted-foreground group-hover:text-primary transition-colors">
|
||||
Read guide <ArrowRight className="w-4 h-4 ml-1.5 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
|
||||
</div>
|
||||
|
||||
<Link to="#" className="absolute inset-0" aria-label={`Read guide for ${guide.title}`} />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Note & CTA Section */}
|
||||
<section className="py-24 bg-secondary/30 border-t border-border/50">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="bg-card border border-border p-8 md:p-12 rounded-3xl relative overflow-hidden flex flex-col items-center text-center"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--accent-purple)/0.05),transparent_60%)] pointer-events-none"></div>
|
||||
|
||||
<div className="w-16 h-16 rounded-full bg-secondary flex items-center justify-center border border-border mb-6 relative z-10">
|
||||
<Info className="w-8 h-8 text-accent-purple" />
|
||||
</div>
|
||||
|
||||
<p className="text-xl text-foreground leading-relaxed mb-8 relative z-10 max-w-2xl text-balance font-medium">
|
||||
Documentation expands as the platform moves from private testing into general availability.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="h-12 px-8 text-base font-semibold border-border hover:bg-secondary transition-all group relative z-10"
|
||||
>
|
||||
<Link to="/contact">
|
||||
Have questions? Contact support
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Shield, Key, MailCheck, Globe, DoorOpen, Fingerprint, Code as CodeXml, Target, Scale, BrainCircuit, Info, ArrowRight } from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.1 }
|
||||
};
|
||||
|
||||
const checks = [
|
||||
{
|
||||
title: "Security headers.",
|
||||
description: "Whether your site tells browsers to enforce HTTPS, block clickjacking, and restrict scripts, the controls that protect your users from a range of common attacks.",
|
||||
icon: Shield,
|
||||
colSpan: "lg:col-span-2",
|
||||
color: "text-blue-400"
|
||||
},
|
||||
{
|
||||
title: "TLS and certificates.",
|
||||
description: "Supported TLS versions, cipher strength, certificate validity, and expiry, so an expired or weak certificate never becomes the thing that breaks trust or traffic.",
|
||||
icon: Key,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-purple-400"
|
||||
},
|
||||
{
|
||||
title: "Email authentication.",
|
||||
description: "SPF, DKIM, DMARC, and MTA-STS, the records that decide whether someone can send mail as your domain. Gaps here are how business email compromise and phishing impersonation begin.",
|
||||
icon: MailCheck,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-emerald-400"
|
||||
},
|
||||
{
|
||||
title: "DNS hygiene.",
|
||||
description: "DNSSEC and CAA, the controls that protect how your domain resolves and who can issue certificates for it.",
|
||||
icon: Globe,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-amber-400"
|
||||
},
|
||||
{
|
||||
title: "Exposed services and ports.",
|
||||
description: "What you are presenting to the internet, including the things you may not realize are exposed.",
|
||||
icon: DoorOpen,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-rose-400"
|
||||
},
|
||||
{
|
||||
title: "Technology and component fingerprinting.",
|
||||
description: "Your content-management system and public components, flagged when they are outdated and known to be vulnerable.",
|
||||
icon: Fingerprint,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-cyan-400"
|
||||
},
|
||||
{
|
||||
title: "Third-party script integrity.",
|
||||
description: "Whether the external scripts your site loads can be tampered with in transit.",
|
||||
icon: CodeXml,
|
||||
colSpan: "lg:col-span-2",
|
||||
color: "text-indigo-400"
|
||||
}
|
||||
];
|
||||
|
||||
export default function ExternalPosturePage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>External Posture Assessment | Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Korrith assesses your internet-facing surface from the outside in, with nothing to install, the way an adversary maps you. See every check it runs."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[75vh] flex flex-col justify-center pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.12),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--primary)/0.05),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<Globe className="w-4 h-4 text-primary" />
|
||||
Layer 1: External Posture
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
See yourself the way an adversary does.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-12">
|
||||
Before an attacker ever touches your systems, they map you from the outside: your domains, your certificates, your exposed services, your email security, the technologies you run. Korrith does the same assessment, the moment you verify a domain, with nothing to install.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why External Posture is Where to Start */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-3xl">
|
||||
<motion.div {...fadeInUp}>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Why external posture is where to start
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-6">
|
||||
It is the highest-signal, lowest-friction view of your security. It needs no access, changes nothing, and yet it covers the exact surface attackers probe first.
|
||||
</p>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed">
|
||||
A weak external posture is often the difference between an attacker moving on and an attacker moving in.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Every Check, Explained (Bento Grid) */}
|
||||
<section className="py-24 bg-secondary/20 border-b border-border/50 relative overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Every check, explained
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
>
|
||||
{checks.map((check, index) => {
|
||||
const Icon = check.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={fadeInUp}
|
||||
className={`h-full ${check.colSpan}`}
|
||||
>
|
||||
<Card className="h-full bg-card/60 border-border/50 hover:border-primary/50 hover:bg-card hover:shadow-lg transition-all duration-300 flex flex-col group overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"></div>
|
||||
<CardHeader>
|
||||
<div className="w-12 h-12 rounded-xl bg-background border border-border flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300 shadow-sm">
|
||||
<Icon className={`w-6 h-6 ${check.color}`} />
|
||||
</div>
|
||||
<CardTitle className="text-xl text-foreground font-semibold leading-tight text-balance">
|
||||
{check.title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="mt-auto">
|
||||
<CardDescription className="text-base text-muted-foreground leading-relaxed">
|
||||
{check.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Zig-Zag Layout */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-32">
|
||||
|
||||
{/* Prioritized by what is actually exploited */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-20 items-center"
|
||||
>
|
||||
<div className="order-2 md:order-1 pr-0 lg:pr-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<Target className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Prioritized by what is actually exploited
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Every finding is weighted with known-exploited and likely-exploited intelligence. A vulnerability that adversaries are actively using is surfaced above one that is theoretically severe but not being weaponized, so your effort goes where the real risk is.
|
||||
</p>
|
||||
</div>
|
||||
<div className="order-1 md:order-2">
|
||||
<div className="relative h-[300px] md:h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.08),transparent_70%)]"></div>
|
||||
<div className="relative z-10 w-full max-w-[280px] space-y-4">
|
||||
<div className="h-16 bg-card border border-primary rounded-xl p-4 flex items-center justify-between shadow-[0_0_20px_-5px_hsl(var(--primary)/0.3)] transform -translate-x-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-primary font-bold text-xs">01</div>
|
||||
<div className="h-3 w-24 bg-foreground/80 rounded"></div>
|
||||
</div>
|
||||
<div className="text-xs font-mono text-primary bg-primary/10 px-2 py-1 rounded">Exploited</div>
|
||||
</div>
|
||||
<div className="h-16 bg-card border border-border rounded-xl p-4 flex items-center justify-between opacity-60">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center text-muted-foreground font-bold text-xs">02</div>
|
||||
<div className="h-3 w-32 bg-muted-foreground/50 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-16 bg-card border border-border rounded-xl p-4 flex items-center justify-between opacity-40 transform translate-x-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center text-muted-foreground font-bold text-xs">03</div>
|
||||
<div className="h-3 w-20 bg-muted-foreground/50 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Honest scoring */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-20 items-center"
|
||||
>
|
||||
<div>
|
||||
<div className="relative h-[300px] md:h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden flex items-center justify-center group">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--emerald)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative z-10 text-center">
|
||||
<div className="w-24 h-24 mx-auto bg-card border-2 border-emerald-500/30 rounded-full flex items-center justify-center shadow-lg relative group-hover:scale-105 transition-transform duration-500">
|
||||
<Scale className="w-10 h-10 text-emerald-400" />
|
||||
<div className="absolute -bottom-2 -right-2 bg-background border border-border rounded-md px-2 py-1 text-xs font-mono text-muted-foreground shadow-sm">
|
||||
N/A
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 space-y-2">
|
||||
<div className="h-2 w-32 bg-muted rounded mx-auto"></div>
|
||||
<div className="h-2 w-24 bg-muted/60 rounded mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-0 lg:pl-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<Scale className="w-8 h-8 text-emerald-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Honest scoring
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Coverage is reported as it is. Where a check could not run or a source was unavailable, Korrith says so rather than implying a clean result. Your score reflects reality, not optimism.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Reasoned, not dumped */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-20 items-center"
|
||||
>
|
||||
<div className="order-2 md:order-1 pr-0 lg:pr-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<BrainCircuit className="w-8 h-8 text-accent-purple" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Reasoned, not dumped
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Findings come back prioritized and explained by Argus, with the why attached, so you can act immediately or hand the list to your team.
|
||||
</p>
|
||||
</div>
|
||||
<div className="order-1 md:order-2">
|
||||
<div className="relative h-[300px] md:h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden flex items-center justify-center group">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--accent-purple)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative z-10 w-full max-w-[320px]">
|
||||
<div className="bg-card border border-border rounded-xl p-5 shadow-lg transform group-hover:-translate-y-2 transition-transform duration-500">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-2 h-2 rounded-full bg-accent-purple"></div>
|
||||
<div className="text-sm font-medium text-foreground">Argus Reasoning</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="h-2 w-full bg-muted-foreground/30 rounded"></div>
|
||||
<div className="h-2 w-5/6 bg-muted-foreground/30 rounded"></div>
|
||||
<div className="h-2 w-4/6 bg-muted-foreground/30 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-8 -right-8 w-32 h-32 bg-accent-purple/10 rounded-full blur-2xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Note Section */}
|
||||
<section className="py-12 bg-background border-t border-border/50">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="flex gap-4 p-6 rounded-2xl bg-secondary/50 border border-border/60"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<Info className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<p className="text-base text-secondary-foreground leading-relaxed">
|
||||
<strong>Note:</strong> the external assessment engine is in private testing.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-card/20 border-t border-border/50 text-center">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Know your true exposure
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10">
|
||||
See what an adversary sees, before they do.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/assessment">
|
||||
Get a free assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { motion } from 'framer-motion';
|
||||
import { HelpCircle, Rocket, Shield, Lock, Database } from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from '@/components/ui/accordion.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const faqSections = [
|
||||
{
|
||||
title: "Getting Started",
|
||||
icon: Rocket,
|
||||
description: "Everything you need to know about setting up and running your first assessment.",
|
||||
questions: [
|
||||
{
|
||||
q: "Do I need to install anything to start?",
|
||||
a: "No. The external assessment runs the moment you verify a domain, with nothing to deploy and nothing changed on your systems."
|
||||
},
|
||||
{
|
||||
q: "What does the free assessment actually check?",
|
||||
a: "Your internet-facing surface: security headers, TLS and certificates, email authentication, DNS hygiene, exposed services and ports, the technologies you run and whether they are outdated, and third-party script integrity, all prioritized by what attackers are actively exploiting."
|
||||
},
|
||||
{
|
||||
q: "Is Korrith available now?",
|
||||
a: "Korrith is built and rolling out. External assessment is in private testing to the beta, and host-internal posture and active validation on your own assets are built and rolling out. Client-facing active testing is gated behind active-testing terms reviewed by counsel. Join the beta for early access."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "The Agent and Your Systems",
|
||||
icon: Shield,
|
||||
description: "Details on our lightweight, read-only host integration.",
|
||||
questions: [
|
||||
{
|
||||
q: "Is the host agent safe to install?",
|
||||
a: "Yes. It is read-only and least-privilege. It reports posture as cryptographically signed evidence, it never modifies your system, it never exfiltrates your data, and it installs only under your explicit, in-product authorization."
|
||||
},
|
||||
{
|
||||
q: "What if I cannot install a server agent?",
|
||||
a: "For platforms like WordPress, a read-only, outbound-only plugin provides the same signed-evidence posture without a server agent."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Active Testing and Safety",
|
||||
icon: Lock,
|
||||
description: "How we ensure our validation engine does no harm.",
|
||||
questions: [
|
||||
{
|
||||
q: "Does Korrith ever attack my systems?",
|
||||
a: "Only under your explicit, signed authorization, only within the exact scope you grant, on the environment you choose, in an approved window, and with a kill-switch that halts everything instantly. The engine cannot act outside that authorization."
|
||||
},
|
||||
{
|
||||
q: "Will active testing lock out my accounts or break things?",
|
||||
a: "No. Active validation is designed to do no harm: it stays well under lockout thresholds and would rather miss a weak credential than lock an account, and it starts with non-destructive checks on owned or signed-scope assets, staging first."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Trust and Data",
|
||||
icon: Database,
|
||||
description: "How we handle, protect, and report on your information.",
|
||||
questions: [
|
||||
{
|
||||
q: "How do you handle my data?",
|
||||
a: "We collect the posture needed to assess and assure and no more. Sensitive values are minimized, confirmed credentials are never stored or shown in plaintext, and end-user personal data is kept out of the inventories we surface."
|
||||
},
|
||||
{
|
||||
q: "Can I trust what the AI tells me?",
|
||||
a: "Argus is constrained to reason only over real findings, and every insight traces back to verifiable evidence. It never invents risk, and unverified results are shown as unverified."
|
||||
},
|
||||
{
|
||||
q: "Can Korrith help with my audit?",
|
||||
a: "Yes. Korrith maps validated findings to common frameworks and produces continuous, audit-ready evidence and proof of resolution."
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export default function FAQPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Frequently Asked Questions | Korrith</title>
|
||||
<meta name="description" content="Clear answers on how Korrith assesses, what the read-only agent does, how active testing is kept safe, how your data is handled, and what is available now." />
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.1),transparent_60%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--accent-purple)/0.08),transparent_60%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<HelpCircle className="w-4 h-4 text-primary" />
|
||||
Help & Resources
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl font-bold tracking-tight text-foreground mb-6 leading-[1.1]">
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-muted-foreground leading-relaxed max-w-2xl">
|
||||
Clear answers on how Korrith assesses your posture, how active validation works, and how we handle your data with zero-trust safety.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Sections */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-24">
|
||||
{faqSections.map((section, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
{...fadeInUp}
|
||||
className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-start"
|
||||
>
|
||||
{/* Category Sidebar */}
|
||||
<div className="lg:col-span-4 lg:sticky lg:top-24">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-xl bg-secondary flex items-center justify-center text-primary">
|
||||
<section.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-foreground">
|
||||
{section.title}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed">
|
||||
{section.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Accordion List */}
|
||||
<div className="lg:col-span-8 bg-card border border-border/60 rounded-3xl p-6 md:p-10 shadow-sm">
|
||||
<Accordion type="single" collapsible className="w-full space-y-4">
|
||||
{section.questions.map((item, qIdx) => {
|
||||
const value = `item-${idx}-${qIdx}`;
|
||||
return (
|
||||
<AccordionItem
|
||||
key={value}
|
||||
value={value}
|
||||
className="border border-border/40 rounded-xl px-6 py-2 bg-background data-[state=open]:bg-secondary/20 data-[state=open]:border-border/80 transition-all"
|
||||
>
|
||||
<AccordionTrigger className="text-left text-lg font-semibold hover:no-underline hover:text-primary transition-colors py-4">
|
||||
{item.q}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground text-base leading-relaxed pb-6 pt-2">
|
||||
{item.a}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
ShieldCheck, Activity, Network, FileSearch, CheckCircle,
|
||||
Database, Lock, Eye, Zap, ChevronRight, Server
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-100px" },
|
||||
transition: { duration: 0.5 }
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Korrith | External Attack Surface and Quantum Readiness</title>
|
||||
<meta name="description" content="Korrith maps your external attack surface and your path to quantum-safe cryptography. In private testing now, platform in active development. Join the beta." />
|
||||
</Helmet>
|
||||
|
||||
{/* 1. HERO SECTION */}
|
||||
<section className="relative min-h-[100dvh] flex items-center pt-20 pb-16 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1675361770627-763fd7dc3e6f?q=80&w=2832&auto=format&fit=crop"
|
||||
alt="Abstract dark grid structures representing network infrastructure"
|
||||
className="w-full h-full object-cover object-center"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-background/90 backdrop-blur-[2px]"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-background/50"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7 }}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary border border-border text-sm font-medium text-secondary-foreground mb-8">
|
||||
<span className="w-2 h-2 rounded-full bg-korrith-ai animate-pulse"></span>
|
||||
Korrith is in active development
|
||||
</div>
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-6 leading-[1.1]">
|
||||
See your external attack surface the way an adversary does.
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-muted-foreground mb-10 max-w-2xl leading-relaxed">
|
||||
Korrith maps your external exposure and your path to quantum-safe cryptography. The assessment engine is in private testing. The full platform is in active development.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Link
|
||||
to="/assessment"
|
||||
className="inline-flex items-center justify-center rounded-lg bg-primary px-8 py-4 text-base font-semibold text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background"
|
||||
>
|
||||
Join the beta
|
||||
</Link>
|
||||
<Link
|
||||
to="/platform/roadmap"
|
||||
className="inline-flex items-center justify-center rounded-lg bg-secondary border border-border px-8 py-4 text-base font-semibold text-secondary-foreground transition-colors hover:bg-secondary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background"
|
||||
>
|
||||
Explore the roadmap
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 2. PILLARS SECTION */}
|
||||
<section className="py-12 border-b border-border/50 bg-card/30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{[
|
||||
{ icon: ShieldCheck, title: "Engineering first", desc: "Tools designed by practitioners, for practitioners." },
|
||||
{ icon: Activity, title: "External visibility", desc: "Your assets mapped exactly as an outside adversary sees them." },
|
||||
{ icon: Lock, title: "Quantum-safe focus", desc: "A clear path toward post-quantum cryptographic standards." },
|
||||
{ icon: Database, title: "Contextual findings", desc: "Remediation paths over raw metric counts." }
|
||||
].map((point, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: i * 0.1 }}
|
||||
className="flex items-start gap-4"
|
||||
>
|
||||
<div className="mt-1 flex-shrink-0 w-10 h-10 rounded-lg bg-secondary border border-border flex items-center justify-center text-primary">
|
||||
<point.icon size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground mb-1">{point.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{point.desc}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. PROBLEM STATEMENT SECTION */}
|
||||
<section className="py-24">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div {...fadeInUp}>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-8">Modern infrastructure is complex.</h2>
|
||||
<div className="text-lg text-muted-foreground space-y-6 text-left md:text-center">
|
||||
<p>
|
||||
Organizations manage growing numbers of domains, IP addresses, and shifting cloud configurations. That sprawl creates an external attack surface that is hard to monitor continuously. Misconfigured headers, expiring certificates, and exposed interfaces stay common. At the same time, the cryptography protecting that data faces a shift. As quantum computing advances, standard public-key cryptography will eventually be vulnerable. Moving to new standards takes years of planning, inventory, and execution. Korrith is being built to address both: a continuous baseline of external risk, and a structured path to post-quantum migration.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 4. WHAT WE ARE PRIORITIZING SECTION (Bento Grid) */}
|
||||
<section className="py-24 bg-card/20 border-y border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">What we are prioritizing</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<motion.div {...fadeInUp} className="md:col-span-2 bg-card border border-border rounded-2xl p-8 lg:p-12 shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 group-hover:bg-primary/10 transition-colors"></div>
|
||||
<Eye className="w-10 h-10 text-primary mb-6" />
|
||||
<h3 className="text-2xl font-bold mb-4 text-card-foreground">Clear visibility</h3>
|
||||
<p className="text-muted-foreground text-lg max-w-md">Map forgotten assets, dangling DNS records, and shadow infrastructure. Security starts with knowing your true perimeter.</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div {...fadeInUp} className="bg-card border border-border rounded-2xl p-8 shadow-sm flex flex-col justify-between">
|
||||
<div>
|
||||
<Lock className="w-8 h-8 text-korrith-ai mb-6" />
|
||||
<h3 className="text-xl font-bold mb-3 text-card-foreground">Cryptographic agility</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground">Audit existing cryptography and outline a path to post-quantum integration.</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div {...fadeInUp} className="bg-card border border-border rounded-2xl p-8 shadow-sm flex flex-col justify-between">
|
||||
<div>
|
||||
<FileSearch className="w-8 h-8 text-korrith-amber mb-6" />
|
||||
<h3 className="text-xl font-bold mb-3 text-card-foreground">Honest reporting</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground">Findings without inflation. Critical severity is reserved for verifiable exposure.</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div {...fadeInUp} className="md:col-span-2 bg-card border border-border rounded-2xl p-8 shadow-sm flex flex-col justify-center">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<Zap className="w-8 h-8 text-korrith-green" />
|
||||
<h3 className="text-xl font-bold text-card-foreground">Actionable context</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-lg">Every flagged issue carries the technical context a team needs to evaluate and fix it.</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 5. ARGUS ENGINE SECTION */}
|
||||
<section className="py-24 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
<motion.div {...fadeInUp}>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary text-sm font-medium mb-6">
|
||||
<Network size={16} />
|
||||
Under Development
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6">The Argus engine</h2>
|
||||
<p className="text-lg text-muted-foreground mb-8">
|
||||
Argus is designed to map your external assets safely. It categorizes services and surfaces common exposure, with no disruptive testing.
|
||||
</p>
|
||||
<ul className="space-y-4">
|
||||
{['Distributed reconnaissance', 'Passive service identification', 'Iterative asset discovery', 'Built for modern cloud environments'].map((feature, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-foreground">
|
||||
<CheckCircle size={18} className="text-korrith-green" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="relative aspect-square md:aspect-video lg:aspect-square bg-card border border-border rounded-2xl overflow-hidden p-6 shadow-2xl flex items-center justify-center radial-glow"
|
||||
>
|
||||
<div className="absolute inset-0 grid grid-cols-8 grid-rows-8 gap-1 opacity-20 pointer-events-none">
|
||||
{Array.from({ length: 64 }).map((_, i) => (
|
||||
<div key={i} className="border border-primary/20 rounded-sm"></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-sm">
|
||||
<div className="flex items-center justify-between mb-4 px-2">
|
||||
<span className="text-xs font-mono text-muted-foreground">Example assessment view (illustrative)</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ target: 'example.production.internal', status: 'active', color: 'bg-korrith-green' },
|
||||
{ target: 'legacy-auth.gateway', status: 'active', color: 'bg-korrith-amber' },
|
||||
{ target: 'vpn.test.net', status: 'active', color: 'bg-korrith-green' },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 rounded-lg bg-secondary border border-border/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server size={16} className="text-muted-foreground" />
|
||||
<span className="font-mono text-sm">{item.target}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${item.color}`}></span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 6. WHAT WE ARE BUILDING SECTION */}
|
||||
<section className="py-24 bg-card/30 border-y border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">What we are building</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">All of the following is in development. We will not claim a capability as ready until it is.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-16">
|
||||
{[
|
||||
{ num: "01", title: "Baseline external assessment", desc: "Automated evaluation of external infrastructure for misconfigurations and exposed services." },
|
||||
{ num: "02", title: "Contextual findings", desc: "Results organized by practical exploitability, so teams focus on what matters." },
|
||||
{ num: "03", title: "Remediation guidance", desc: "Technical context and configuration examples for each gap." },
|
||||
{ num: "04", title: "Quantum readiness mapping", desc: "Inventory current cryptography and model a phased post-quantum transition." },
|
||||
{ num: "05", title: "Continuous monitoring and host-level posture", desc: "" }
|
||||
].map((item, i) => (
|
||||
<motion.div key={i} {...fadeInUp} className="flex gap-6">
|
||||
<div className="text-5xl md:text-6xl font-bold text-primary/20 tracking-tighter">
|
||||
{item.num}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-3">{item.title}</h3>
|
||||
{item.desc && <p className="text-muted-foreground leading-relaxed">{item.desc}</p>}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 7. QUANTUM READINESS SECTION */}
|
||||
<section className="py-24 border-y border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="relative aspect-[4/3] rounded-2xl overflow-hidden shadow-2xl order-2 lg:order-1"
|
||||
>
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1667371927761-8fa90f33a248?q=80&w=2832&auto=format&fit=crop"
|
||||
alt="Quantum computing hardware visualization"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-background/40 mix-blend-multiply"></div>
|
||||
<div className="absolute inset-0 border border-border/50 rounded-2xl"></div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div {...fadeInUp} className="order-1 lg:order-2">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6">Preparing for the cryptographic shift.</h2>
|
||||
<p className="text-lg text-muted-foreground mb-8">
|
||||
As computing power advances, current public-key encryption will eventually face real risk. Migrating to post-quantum cryptography is a multi-year effort for most organizations. Korrith is being built to help you find where vulnerable cryptography lives in your infrastructure and model a structured transition.
|
||||
</p>
|
||||
<Link
|
||||
to="/platform/quantum-readiness"
|
||||
className="inline-flex items-center justify-center rounded-lg bg-secondary border border-border px-6 py-3 text-sm font-semibold text-secondary-foreground transition-colors hover:bg-secondary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background"
|
||||
>
|
||||
Read our approach
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 8. CALL-TO-ACTION SECTION */}
|
||||
<section className="py-24 relative overflow-hidden radial-glow bg-card/20 border-b border-border/50">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||
<motion.div {...fadeInUp}>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-6">Follow our progress</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10 max-w-2xl mx-auto">
|
||||
Korrith is in active development. Join the beta to help validate the platform on real infrastructure and shape what we build.
|
||||
</p>
|
||||
<Link
|
||||
to="/assessment"
|
||||
className="inline-flex items-center justify-center rounded-lg bg-primary px-8 py-4 text-base font-semibold text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)]"
|
||||
>
|
||||
Join the beta
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 9. FAQ SECTION */}
|
||||
<section className="py-24">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold mb-4">Frequently asked questions</h2>
|
||||
<p className="text-muted-foreground">Information about our development stage and platform approach.</p>
|
||||
</div>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{[
|
||||
{
|
||||
q: "What stage is Korrith in?",
|
||||
a: "Korrith is in active development. The external posture engine is in private testing. The broader platform is in progress."
|
||||
},
|
||||
{
|
||||
q: "How does the external assessment work?",
|
||||
a: "Argus performs passive reconnaissance and configuration analysis of your public-facing surface, then grades it and explains each finding with evidence. It does not perform intrusive or disruptive testing."
|
||||
},
|
||||
{
|
||||
q: "What is your approach to post-quantum cryptography?",
|
||||
a: "We assess where vulnerable cryptography exists and help model a phased migration. This is a readiness view, not a guarantee."
|
||||
},
|
||||
{
|
||||
q: "How can I take part?",
|
||||
a: "Join the beta. We are onboarding a small group to validate the platform on real infrastructure."
|
||||
}
|
||||
].map((faq, i) => (
|
||||
<AccordionItem key={i} value={`item-${i}`} className="border-border/50">
|
||||
<AccordionTrigger className="text-left font-medium hover:text-primary transition-colors py-5 text-lg">
|
||||
{faq.q}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground leading-relaxed pb-5 text-base">
|
||||
{faq.a}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 10. CLOSING SECTION */}
|
||||
<section className="py-24 relative overflow-hidden border-t border-border/50 bg-card/20">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div {...fadeInUp}>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-6">Secure infrastructure. Quantum future.</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10 max-w-2xl mx-auto">
|
||||
Korrith is being built to bring external posture assessment and quantum readiness into one honest platform.
|
||||
</p>
|
||||
<Link
|
||||
to="/assessment"
|
||||
className="inline-flex items-center justify-center rounded-lg bg-primary px-8 py-4 text-base font-semibold text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background"
|
||||
>
|
||||
Join the beta
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Server,
|
||||
PackageSearch,
|
||||
KeyRound,
|
||||
Settings2,
|
||||
UserCheck,
|
||||
LockKeyhole,
|
||||
HardDrive,
|
||||
BugOff,
|
||||
FileSignature,
|
||||
ShieldBan,
|
||||
PenTool,
|
||||
Network,
|
||||
Info,
|
||||
ArrowRight
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.1 }
|
||||
};
|
||||
|
||||
const capabilities = [
|
||||
{
|
||||
title: "Software bill of materials",
|
||||
description: "Software bill of materials and known vulnerabilities, prioritized by what is actually being exploited.",
|
||||
icon: PackageSearch,
|
||||
colSpan: "lg:col-span-2",
|
||||
color: "text-blue-400",
|
||||
bg: "bg-blue-400/10"
|
||||
},
|
||||
{
|
||||
title: "Exposed secrets",
|
||||
description: "Exposed secrets in code and configuration.",
|
||||
icon: KeyRound,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-amber-400",
|
||||
bg: "bg-amber-400/10"
|
||||
},
|
||||
{
|
||||
title: "Configuration posture",
|
||||
description: "Configuration posture against recognized benchmarks.",
|
||||
icon: Settings2,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-purple-400",
|
||||
bg: "bg-purple-400/10"
|
||||
},
|
||||
{
|
||||
title: "Identity and access",
|
||||
description: "Identity and access posture, with file-integrity monitoring.",
|
||||
icon: UserCheck,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-emerald-400",
|
||||
bg: "bg-emerald-400/10"
|
||||
},
|
||||
{
|
||||
title: "Cryptographic inventory",
|
||||
description: "Cryptographic inventory and quantum exposure.",
|
||||
icon: LockKeyhole,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-cyan-400",
|
||||
bg: "bg-cyan-400/10"
|
||||
},
|
||||
{
|
||||
title: "Operating-system patch level",
|
||||
description: "Operating-system patch level and end-of-life status.",
|
||||
icon: HardDrive,
|
||||
colSpan: "lg:col-span-1",
|
||||
color: "text-indigo-400",
|
||||
bg: "bg-indigo-400/10"
|
||||
},
|
||||
{
|
||||
title: "Host malware indicators",
|
||||
description: "Host malware indicators.",
|
||||
icon: BugOff,
|
||||
colSpan: "lg:col-span-2",
|
||||
color: "text-rose-400",
|
||||
bg: "bg-rose-400/10"
|
||||
}
|
||||
];
|
||||
|
||||
export default function InternalPosturePage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Internal Posture and the Host Agent | Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A read-only, least-privilege agent reports your host's real posture as cryptographically signed evidence. See what an attacker finds once past the perimeter."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[75vh] flex flex-col justify-center pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--accent-purple)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--primary)/0.08),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<Server className="w-4 h-4 text-accent-purple" />
|
||||
Layer 2: Internal Posture
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
See what an attacker finds past the perimeter.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-12">
|
||||
External posture shows what an adversary sees from outside. Internal posture shows what they would find once inside: the vulnerable package, the exposed secret, the drifted configuration, the over-privileged account. Korrith reports all of it through a read-only, least-privilege agent, as cryptographically signed evidence.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Agent Section (Bento Grid) */}
|
||||
<section className="py-24 bg-secondary/20 border-b border-border/50 relative overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="mb-16 max-w-3xl"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
The agent, and what it reads
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed">
|
||||
A single read-only server agent, with a read-only, outbound-only WordPress plugin for sites where a server agent is not appropriate, reports your host's real posture:
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-6"
|
||||
>
|
||||
{capabilities.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={fadeInUp}
|
||||
className={`group relative p-6 md:p-8 rounded-3xl bg-card border border-border/60 hover:border-primary/40 transition-colors duration-300 overflow-hidden ${item.colSpan}`}
|
||||
>
|
||||
<div className="absolute top-0 right-0 -mt-4 -mr-4 w-32 h-32 bg-gradient-to-br from-primary/5 to-transparent rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full">
|
||||
<div className={`w-12 h-12 rounded-2xl ${item.bg} border border-border/50 flex items-center justify-center mb-6 shadow-sm group-hover:scale-110 transition-transform duration-300`}>
|
||||
<Icon className={`w-6 h-6 ${item.color}`} />
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Feature Sections - Zig Zag */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-32">
|
||||
|
||||
{/* Signed evidence you can trust */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-20 items-center"
|
||||
>
|
||||
<div className="order-2 md:order-1 pr-0 lg:pr-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<FileSignature className="w-8 h-8 text-emerald-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Signed evidence you can trust
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Every reading the agent sends is cryptographically signed, so the platform can prove the evidence is authentic and untampered.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
The signing contract holds identically across the server agent and the WordPress plugin, which means the same guarantee whether a host runs a full agent or a plugin.
|
||||
</p>
|
||||
</div>
|
||||
<div className="order-1 md:order-2">
|
||||
<div className="relative h-[350px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden flex items-center justify-center group">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--emerald)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative z-10 w-full max-w-[320px] p-6 bg-card border border-border rounded-xl shadow-2xl transform group-hover:scale-105 transition-transform duration-500">
|
||||
<div className="flex items-center gap-3 mb-6 border-b border-border pb-4">
|
||||
<FileSignature className="w-5 h-5 text-emerald-400" />
|
||||
<span className="text-sm font-mono text-emerald-400">Signature Verified</span>
|
||||
</div>
|
||||
<div className="space-y-3 font-mono text-xs text-muted-foreground">
|
||||
<div className="flex justify-between">
|
||||
<span>Payload:</span>
|
||||
<span className="text-foreground">host_posture_v2</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Timestamp:</span>
|
||||
<span className="text-foreground">2026-06-22T14:02:11Z</span>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border/50 break-all text-emerald-400/60 leading-relaxed">
|
||||
eyJBb...GciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* What it never does */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-20 items-center"
|
||||
>
|
||||
<div>
|
||||
<div className="relative h-[350px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden flex items-center justify-center group">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--rose)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative z-10 flex flex-col gap-4">
|
||||
{["Modify your system", "Exfiltrate your data", "Act without authorization"].map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-4 bg-card border border-rose-500/20 px-6 py-4 rounded-xl shadow-sm transform group-hover:-translate-x-2 transition-transform duration-500" style={{ transitionDelay: `${i * 100}ms` }}>
|
||||
<ShieldBan className="w-5 h-5 text-rose-400 shrink-0" />
|
||||
<span className="text-sm font-medium text-foreground">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-0 lg:pl-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<ShieldBan className="w-8 h-8 text-rose-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
What it never does
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
It is read-only and least-privilege by design. It does not modify your system. It does not exfiltrate your data. It does not act without authorization.
|
||||
</p>
|
||||
<p className="text-lg text-foreground font-medium leading-relaxed mt-4">
|
||||
It reads posture and reports it, nothing more.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Authorized, in product */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-20 items-center"
|
||||
>
|
||||
<div className="order-2 md:order-1 pr-0 lg:pr-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<PenTool className="w-8 h-8 text-blue-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Authorized, in product
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
The agent installs only through Korrith's in-product, two-party authorization: the asset owner reviews a versioned consent and e-signs, and the operator countersigns.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
No paper side-channel, no implicit access. The authorization record is the agent's permission.
|
||||
</p>
|
||||
</div>
|
||||
<div className="order-1 md:order-2">
|
||||
<div className="relative h-[350px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden flex items-center justify-center group">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--blue)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative z-10 w-full max-w-[280px]">
|
||||
<div className="bg-card border border-border rounded-xl p-5 shadow-lg transform -rotate-2 group-hover:rotate-0 transition-transform duration-500 relative z-20">
|
||||
<div className="flex items-center justify-between mb-4 border-b border-border pb-3">
|
||||
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Asset Owner</span>
|
||||
<div className="w-4 h-4 rounded-full bg-blue-500/20 border border-blue-500 flex items-center justify-center">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 w-3/4 bg-muted rounded mb-2"></div>
|
||||
<div className="h-2 w-1/2 bg-muted rounded mb-6"></div>
|
||||
<div className="font-mono text-[10px] text-blue-400/80">Signed: e3b0c442...</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0b0f18] border border-border rounded-xl p-5 shadow-lg transform rotate-3 translate-x-4 -translate-y-6 group-hover:rotate-0 transition-transform duration-500 relative z-10">
|
||||
<div className="flex items-center justify-between mb-4 border-b border-border pb-3">
|
||||
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Operator</span>
|
||||
<div className="w-4 h-4 rounded-full bg-emerald-500/20 border border-emerald-500 flex items-center justify-center">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 w-2/3 bg-muted rounded mb-2"></div>
|
||||
<div className="h-2 w-1/2 bg-muted rounded mb-6"></div>
|
||||
<div className="font-mono text-[10px] text-emerald-400/80">Countersigned: f2d1a334...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Part of one story */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-20 items-center"
|
||||
>
|
||||
<div>
|
||||
<div className="relative h-[350px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden flex items-center justify-center group">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--accent-purple)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative z-10 flex flex-col items-center">
|
||||
<div className="flex gap-8 mb-8">
|
||||
<div className="w-16 h-16 rounded-xl bg-card border border-border flex items-center justify-center shadow-sm">
|
||||
<Network className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="w-16 h-16 rounded-xl bg-card border border-border flex items-center justify-center shadow-sm">
|
||||
<Server className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flow lines */}
|
||||
<div className="flex gap-16 mb-4">
|
||||
<div className="w-px h-8 bg-gradient-to-b from-border to-primary"></div>
|
||||
<div className="w-px h-8 bg-gradient-to-b from-border to-primary"></div>
|
||||
</div>
|
||||
|
||||
<div className="w-32 h-16 rounded-xl bg-primary/10 border-2 border-primary flex items-center justify-center shadow-[0_0_20px_-5px_hsl(var(--primary)/0.3)]">
|
||||
<span className="font-semibold text-primary">Argus</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-0 lg:pl-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<Network className="w-8 h-8 text-accent-purple" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Part of one story
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Internal posture findings flow into the same scored, prioritized, Argus-reasoned view as everything else, and into your reports.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mt-4">
|
||||
Host-internal exposure is part of one prioritized picture, not a separate tool to reconcile.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Note Section */}
|
||||
<section className="py-12 bg-background border-t border-border/50">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="flex gap-4 p-6 rounded-2xl bg-secondary/50 border border-border/60"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<Info className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<p className="text-base text-secondary-foreground leading-relaxed">
|
||||
<strong>Note:</strong> built and rolling out to the beta.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-card/20 border-t border-border/50 text-center">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Secure the inside out
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10">
|
||||
Get cryptographically signed evidence of your real internal posture.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/assessment">
|
||||
Get a free assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowLeft, Lock } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useToast } from '@/hooks/use-toast.js';
|
||||
|
||||
function LoginPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email || !password) {
|
||||
toast({
|
||||
title: "Missing credentials",
|
||||
description: "Please enter both email and password.",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate login attempt
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
toast({
|
||||
title: "Login feature coming soon",
|
||||
description: "Authentication system is currently in development."
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Login - Korrith</title>
|
||||
<meta name="description" content="Sign in to your Korrith account to access quantum-ready security infrastructure." />
|
||||
</Helmet>
|
||||
|
||||
<div className="min-h-[100dvh] bg-background flex items-center justify-center px-4 sm:px-6 lg:px-8 relative overflow-hidden">
|
||||
{/* Background pattern */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.05),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="relative w-full max-w-md z-10"
|
||||
>
|
||||
{/* Back to home link */}
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors duration-200 mb-8"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to home
|
||||
</Link>
|
||||
|
||||
{/* Login card */}
|
||||
<div className="bg-card rounded-2xl shadow-lg p-8 border border-border">
|
||||
{/* Logo and header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary/10 rounded-xl mb-4">
|
||||
<Lock className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold mb-2 text-foreground">Welcome back</h1>
|
||||
<p className="text-muted-foreground">Sign in to your Korrith account</p>
|
||||
</div>
|
||||
|
||||
{/* Login form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-foreground">
|
||||
Email address
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@company.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="bg-background border-border h-12 text-foreground placeholder:text-muted-foreground/50"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password" className="text-foreground">
|
||||
Password
|
||||
</Label>
|
||||
<Link
|
||||
to="#"
|
||||
className="text-sm text-primary hover:text-primary/80 transition-colors duration-200"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="bg-background border-border h-12 text-foreground placeholder:text-muted-foreground/50"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-12 bg-primary text-primary-foreground hover:bg-primary/90 transition-all duration-200 active:scale-[0.98]"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-border"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-card text-muted-foreground">
|
||||
New to Korrith?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign up link */}
|
||||
<div className="text-center">
|
||||
<Link
|
||||
to="/assessment"
|
||||
className="text-sm text-primary hover:text-primary/80 transition-colors duration-200"
|
||||
>
|
||||
Request team access
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security notice */}
|
||||
<p className="text-center text-sm text-muted-foreground mt-6">
|
||||
Protected by quantum-resistant encryption
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,380 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Network,
|
||||
ArrowRight,
|
||||
ShieldCheck,
|
||||
Search,
|
||||
Wrench,
|
||||
RefreshCw,
|
||||
FileCheck,
|
||||
Repeat,
|
||||
Lock,
|
||||
Layers,
|
||||
Activity,
|
||||
Workflow,
|
||||
ServerCog,
|
||||
Briefcase
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
const loopSteps = [
|
||||
{ icon: Search, label: "Find" },
|
||||
{ icon: ShieldCheck, label: "Validate" },
|
||||
{ icon: Wrench, label: "Remediate" },
|
||||
{ icon: RefreshCw, label: "Re-validate" },
|
||||
{ icon: FileCheck, label: "Prove" },
|
||||
{ icon: Repeat, label: "Continuously", isAccent: true }
|
||||
];
|
||||
|
||||
export default function MSSPsPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Korrith for MSSPs | Multi-client Assessment and Assurance at Scale</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Korrith gives MSSPs multi-client assessment, validation, and assurance with strict per-client boundaries, audit-ready reporting, and a closed loop that proves resolution."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section with Background Image */}
|
||||
<section className="relative pt-32 pb-24 md:pt-40 md:pb-32 overflow-hidden border-b border-border/50 min-h-[90vh] flex items-center">
|
||||
{/* Background Image & Overlays */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1688413399498-e35ed74b554f"
|
||||
alt="Abstract architecture representing complex structured networks"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-background/90 backdrop-blur-[2px]"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background via-background/95 to-background/40"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<Network className="w-4 h-4 text-primary" />
|
||||
For MSSPs
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
Run assurance for every client, and prove it.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-2xl mb-10">
|
||||
You already manage security for many clients. Korrith gives you a clean way to assess, validate, and continuously assure all of them, with strict per-client boundaries, an honest score you can stand behind, and reporting your clients can hand to their own auditors.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group">
|
||||
<Link to="/contact">
|
||||
Become a partner
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Built for many tenants (Bento Grid) */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-6 text-balance">
|
||||
Built for many tenants
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
||||
>
|
||||
{/* Bento Item 1: Boundaries (Col span 2) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="md:col-span-2 bg-card border border-border p-8 md:p-10 rounded-3xl hover:bg-secondary/40 transition-colors flex flex-col justify-between group"
|
||||
>
|
||||
<div>
|
||||
<div className="w-14 h-14 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center mb-6 group-hover:scale-105 transition-transform duration-300">
|
||||
<Lock className="w-7 h-7 text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-foreground mb-4">Strict client boundaries</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed max-w-2xl">
|
||||
Per-client scoping is enforced in the platform, not left to process. A client's data, findings, and reports stay theirs. Each client sees only their own posture; you see the estate.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Bento Item 2: One Operator View (Col span 1) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="md:col-span-1 bg-card border border-border p-8 md:p-10 rounded-3xl hover:bg-secondary/40 transition-colors flex flex-col group"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center mb-6 group-hover:scale-105 transition-transform duration-300">
|
||||
<Layers className="w-7 h-7 text-purple-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-foreground mb-3">One operator view</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed">
|
||||
Across your whole book, you see what regressed, what is aging, and where each client sits, so you can prioritize your team's time where the real risk is.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Bento Item 3: Honest Scoring (Col span 3) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="md:col-span-3 bg-secondary/30 border border-border p-8 md:p-10 rounded-3xl hover:bg-secondary/50 transition-colors flex flex-col md:flex-row gap-8 items-start md:items-center relative overflow-hidden group"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-300 relative z-10">
|
||||
<Activity className="w-7 h-7 text-emerald-400" />
|
||||
</div>
|
||||
<div className="relative z-10 flex-1">
|
||||
<h3 className="text-xl md:text-2xl font-semibold text-foreground mb-2">Honest scoring everywhere</h3>
|
||||
<p className="text-muted-foreground text-lg max-w-4xl leading-relaxed">
|
||||
No fake-clean, per client, so a quiet gap never hides behind a green dashboard.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Closed Loop Section */}
|
||||
<section className="py-24 bg-secondary/10 border-b border-border/50 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center"
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
The closed loop is your recurring value
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mb-8">
|
||||
The hardest part of an MSSP relationship is demonstrating ongoing value. Korrith's loop does it for you: find, validate, remediate, re-validate, prove, continuously. You can show each client not just what you found, but what you proved fixed, with dates and evidence. That is a renewal conversation that runs itself.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Loop Diagram */}
|
||||
<div className="relative bg-card border border-border rounded-3xl p-8 md:p-12 shadow-xl shadow-background/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.05),transparent_70%)] pointer-events-none rounded-3xl"></div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 relative z-10">
|
||||
{loopSteps.map((step, idx) => (
|
||||
<React.Fragment key={step.label}>
|
||||
<div className={`flex flex-col items-center gap-3 w-[100px] ${step.isAccent ? 'scale-110' : ''}`}>
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center border shadow-sm transition-colors ${
|
||||
step.isAccent
|
||||
? 'bg-primary border-primary text-primary-foreground shadow-[0_0_20px_-5px_hsl(var(--primary)/0.5)]'
|
||||
: 'bg-background border-border text-foreground hover:border-primary/50'
|
||||
}`}>
|
||||
<step.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<span className={`text-sm font-medium text-center ${step.isAccent ? 'text-primary' : 'text-muted-foreground'}`}>
|
||||
{step.label}
|
||||
</span>
|
||||
</div>
|
||||
{idx < loopSteps.length - 1 && (
|
||||
<div className="hidden sm:flex text-muted-foreground/30 px-1">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Active Validation, Safely (Image Background Section) */}
|
||||
<section className="relative py-32 border-b border-border/50">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1665659964378-cbccc8a6d429"
|
||||
alt="Secure technological infrastructure visualization"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-background/95 backdrop-blur-sm"></div>
|
||||
{/* Subtle gradient overlay to enhance text readability and match theme */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-background/80"></div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-2xl bg-rose-500/10 border border-rose-500/20 flex items-center justify-center mb-8">
|
||||
<ShieldCheck className="w-8 h-8 text-rose-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-6 text-balance">
|
||||
Active validation, safely
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed">
|
||||
Korrith's active testing is gated by cryptographically signed authorization and a kill-switch, scoped per target, and designed for owned or signed-scope assets on staging first. You get the depth of validation your clients need without the operational and legal risk that usually comes with offensive testing.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Assess and Route Section */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center">
|
||||
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="order-2 lg:order-1 relative aspect-square lg:aspect-auto lg:h-[400px] rounded-3xl bg-secondary/30 border border-border flex items-center justify-center p-8 overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--purple-500)/0.05),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
{/* Visual showing routing flow */}
|
||||
<div className="flex flex-col items-center gap-6 relative z-10 w-full max-w-sm">
|
||||
|
||||
{/* MSSP Bubble */}
|
||||
<div className="w-full bg-card border border-border rounded-xl p-4 flex items-center gap-4 shadow-lg">
|
||||
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<Briefcase className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-foreground">MSSP Relationship</div>
|
||||
<div className="text-xs text-muted-foreground">Client assurance layer</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-12 w-full justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-[2px] h-8 bg-border"></div>
|
||||
<ArrowRight className="w-5 h-5 text-muted-foreground rotate-90" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-[2px] h-8 bg-purple-500/50"></div>
|
||||
<ArrowRight className="w-5 h-5 text-purple-400 rotate-90" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 w-full">
|
||||
{/* Standard Work */}
|
||||
<div className="flex-1 bg-card/50 border border-border rounded-xl p-4 flex flex-col items-center text-center gap-2 opacity-70">
|
||||
<Wrench className="w-5 h-5 text-muted-foreground" />
|
||||
<div className="text-xs font-medium">Standard Fixes</div>
|
||||
</div>
|
||||
|
||||
{/* Specialized Work routed to Korrith */}
|
||||
<div className="flex-1 bg-purple-500/10 border border-purple-500/30 rounded-xl p-4 flex flex-col items-center text-center gap-2 shadow-[0_0_20px_-5px_hsl(var(--purple-500)/0.2)]">
|
||||
<ServerCog className="w-5 h-5 text-purple-400" />
|
||||
<div className="text-xs font-medium text-purple-200">Specialist Routing</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="order-1 lg:order-2"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full bg-purple-500/10 border border-purple-500/20 flex items-center justify-center mb-8">
|
||||
<Workflow className="w-8 h-8 text-purple-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Assess and route
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Where remediation calls for specialized work, such as post-quantum cryptography, Korrith assesses and routes the relevant subset while the assurance relationship stays with you. You keep the client and the recurring revenue; the specialist work happens underneath.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What You Get Summary Section */}
|
||||
<section className="py-24 bg-secondary/20">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
What you get
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-12 max-w-2xl mx-auto">
|
||||
A multi-client assurance platform that scales with your book, proves your value continuously, and produces evidence your clients can actually use.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 text-left mb-12">
|
||||
<div className="bg-card border border-border p-6 rounded-2xl">
|
||||
<div className="text-primary mb-4"><Layers className="w-6 h-6" /></div>
|
||||
<div className="font-semibold text-foreground mb-2">Scalable Platform</div>
|
||||
<div className="text-sm text-muted-foreground">Manage your entire book of business securely from a single view.</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border p-6 rounded-2xl">
|
||||
<div className="text-emerald-400 mb-4"><Repeat className="w-6 h-6" /></div>
|
||||
<div className="font-semibold text-foreground mb-2">Continuous Value</div>
|
||||
<div className="text-sm text-muted-foreground">Turn one-off audits into defensible, recurring revenue streams.</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border p-6 rounded-2xl">
|
||||
<div className="text-blue-400 mb-4"><FileCheck className="w-6 h-6" /></div>
|
||||
<div className="font-semibold text-foreground mb-2">Audit-ready Evidence</div>
|
||||
<div className="text-sm text-muted-foreground">Export clear, validated proof of posture and resolution for clients.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="w-full sm:w-auto h-14 px-10 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/contact">
|
||||
Become an MSSP partner
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
|
||||
export default function ManagedSecureHostingPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet><title>Managed Secure Hosting | Korrith</title></Helmet>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-foreground">Managed Secure Hosting</h1>
|
||||
<p className="mt-4 text-muted-foreground text-lg">Content coming soon in Phase 2.</p>
|
||||
</div>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRight, HeartHandshake as Handshake, Briefcase, ShieldCheck, Cpu, Repeat, FileCheck, Award } from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.1 }
|
||||
};
|
||||
|
||||
export default function PartnersPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Partner with Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Agencies, MSSPs, and technology partners can all build on Korrith. A recurring, defensible security line, with the hard parts routed to the right capability."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[60vh] flex flex-col justify-center pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.1),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--accent-purple)/0.08),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<Handshake className="w-4 h-4 text-primary" />
|
||||
Partner Ecosystem
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 leading-[1.1]">
|
||||
Build on Korrith.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-muted-foreground leading-relaxed max-w-2xl mb-12">
|
||||
Whether you are an agency, a managed security provider, or a technology partner, there is a way to build a real, recurring business on Korrith. You bring the relationship; Korrith brings the assessment, the validation, and the proof.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-korrith group"
|
||||
>
|
||||
<Link to="/contact">
|
||||
Become a partner
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Partner Types Section (2+1 Bento Layout to avoid generic 3-card grid) */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp} className="mb-16 max-w-2xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
|
||||
A model for every capability
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
We designed our platform to integrate neatly into your existing business model, removing the friction from offering real security assurance.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8"
|
||||
>
|
||||
{/* Card 1: Agencies (col-span-1) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="group bg-card border border-border/50 rounded-3xl p-8 md:p-10 hover:border-primary/50 hover:bg-secondary/20 transition-korrith flex flex-col h-full relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 group-hover:bg-primary/10 transition-colors"></div>
|
||||
|
||||
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-secondary border border-border mb-8 relative z-10">
|
||||
<Briefcase className="w-7 h-7 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-card-foreground mb-4 relative z-10">
|
||||
For agencies
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed mb-10 relative z-10">
|
||||
Add security assessment and assurance to what you already deliver for clients, with per-client reporting, no security team required, and a recurring fee for something clients can see working.
|
||||
</p>
|
||||
<div className="mt-auto pt-6 border-t border-border/40 relative z-10">
|
||||
<Link to="/solutions/agencies" className="inline-flex items-center text-primary font-medium hover:text-primary/80 transition-colors">
|
||||
See For Agencies <ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Card 2: MSSPs (col-span-1) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="group bg-card border border-border/50 rounded-3xl p-8 md:p-10 hover:border-accent-purple/50 hover:bg-secondary/20 transition-korrith flex flex-col h-full relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-accent-purple/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 group-hover:bg-accent-purple/10 transition-colors"></div>
|
||||
|
||||
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-secondary border border-border mb-8 relative z-10">
|
||||
<ShieldCheck className="w-7 h-7 text-accent-purple" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-card-foreground mb-4 relative z-10">
|
||||
For MSSPs
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed mb-10 relative z-10">
|
||||
Run multi-client assessment and assurance at scale, with strict per-client boundaries, audit-ready reporting, and a closed loop that proves your value at every renewal.
|
||||
</p>
|
||||
<div className="mt-auto pt-6 border-t border-border/40 relative z-10">
|
||||
<Link to="/solutions/mssps" className="inline-flex items-center text-accent-purple font-medium hover:text-accent-purple/80 transition-colors">
|
||||
See For MSSPs <ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Card 3: Tech Partners (col-span-1 md:col-span-2 full width) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="group bg-card/60 border border-border/50 rounded-3xl p-8 md:p-12 hover:border-border hover:bg-secondary/30 transition-korrith md:col-span-2 grid grid-cols-1 md:grid-cols-12 gap-8 items-center overflow-hidden relative"
|
||||
>
|
||||
<div className="absolute bottom-0 left-1/2 w-[800px] h-[300px] bg-primary/5 rounded-[100%] blur-3xl translate-y-1/2 -translate-x-1/2 pointer-events-none"></div>
|
||||
|
||||
<div className="md:col-span-5 flex flex-col relative z-10">
|
||||
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-secondary border border-border mb-8">
|
||||
<Cpu className="w-7 h-7 text-foreground" />
|
||||
</div>
|
||||
<h3 className="text-2xl md:text-3xl font-bold text-card-foreground mb-4">
|
||||
For technology partners
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-7 relative z-10">
|
||||
<p className="text-muted-foreground text-lg md:text-xl leading-relaxed mb-8">
|
||||
Korrith assesses and routes. Where remediation calls for specialized work, such as post-quantum cryptography, Korrith identifies the relevant subset and routes it to the right capability while the assurance relationship stays in place.
|
||||
<br/><br/>
|
||||
If you provide a specialized remediation capability, there is a natural place for you in that flow.
|
||||
</p>
|
||||
<Button variant="outline" className="group/btn" asChild>
|
||||
<Link to="/contact">
|
||||
Discuss integrations
|
||||
<ArrowRight className="ml-2 w-4 h-4 group-hover/btn:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What Partners Share Section */}
|
||||
<section className="py-24 bg-secondary/30 border-y border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start">
|
||||
<motion.div {...fadeInUp} className="lg:col-span-4 lg:sticky lg:top-24">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
What partners share
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Regardless of how you go to market, partnering with Korrith establishes a resilient, evidence-backed foundation for your services.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="lg:col-span-8 space-y-6"
|
||||
>
|
||||
{[
|
||||
{
|
||||
icon: Repeat,
|
||||
title: "A recurring relationship",
|
||||
desc: "Move away from one-time transactional engagements and build continuous, measurable security value."
|
||||
},
|
||||
{
|
||||
icon: FileCheck,
|
||||
title: "Reporting and proof",
|
||||
desc: "Deliver tangible, audit-ready evidence that your clients can confidently use with their own stakeholders and boards."
|
||||
},
|
||||
{
|
||||
icon: Award,
|
||||
title: "Evidence-backed credibility",
|
||||
desc: "Leverage the trust of an honest, validated platform behind your brand, proving outcomes instead of promising them."
|
||||
}
|
||||
].map((item, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={fadeInUp}
|
||||
className="bg-card border border-border/50 rounded-2xl p-6 md:p-8 flex gap-6 items-start hover:bg-card/80 transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-xl bg-secondary/80 flex items-center justify-center text-primary mt-1 border border-border/40">
|
||||
<item.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<section className="py-24 bg-background text-center">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp}>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6">
|
||||
Ready to build?
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10">
|
||||
Join the partners already building defensible, recurring revenue on the Korrith platform.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold transition-korrith group"
|
||||
>
|
||||
<Link to="/contact">
|
||||
Become a partner
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
|
||||
export default function PostQuantumReadinessPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet><title>Post-Quantum Readiness | Korrith</title></Helmet>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-foreground">Post-Quantum Readiness</h1>
|
||||
<p className="mt-4 text-muted-foreground text-lg">Content coming soon in Phase 2.</p>
|
||||
</div>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Privacy Policy | Korrith</title>
|
||||
<meta name="description" content="Korrith privacy policy. Learn how we plan to collect, use, and protect your data as we develop our platform." />
|
||||
</Helmet>
|
||||
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
||||
<div className="mb-12">
|
||||
<h1 className="text-4xl font-bold text-foreground mb-4">Privacy Policy</h1>
|
||||
<p className="text-muted-foreground font-mono text-sm">Last updated: 2026-06-16</p>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-invert prose-p:text-muted-foreground prose-h2:text-foreground prose-h2:mt-12 prose-h2:mb-4 prose-h2:text-2xl prose-h2:font-semibold prose-a:text-primary hover:prose-a:text-primary/80 max-w-none">
|
||||
<p>
|
||||
At Korrith, we take your privacy and data security seriously. As we develop our platform, this Privacy Policy describes how we intend to collect, use, maintain, and disclose information collected from future users of our services and beta testers.
|
||||
</p>
|
||||
|
||||
<h2>Introduction</h2>
|
||||
<p>
|
||||
By accessing or using the Korrith website and participating in any beta programs, you agree to the practices described in this policy. We are building our systems to prioritize minimal data collection and adhere to strict retention schedules for all security posture data gathered during authorized scans.
|
||||
</p>
|
||||
|
||||
<h2>Information we collect</h2>
|
||||
<p>
|
||||
We collect information that you directly provide to us when requesting beta access or contacting us, including your name, email address, and company name. When our platform becomes active, we will also collect technical data about the domains and infrastructure you explicitly authorize us to assess.
|
||||
</p>
|
||||
<p>
|
||||
This future technical data will include DNS records, TLS certificates, HTTP headers, and public-facing service configurations.
|
||||
</p>
|
||||
|
||||
<h2>How we use your information</h2>
|
||||
<p>
|
||||
The information we collect is used strictly for the following purposes:
|
||||
</p>
|
||||
<ul>
|
||||
<li>To manage beta waitlists and communicate development updates</li>
|
||||
<li>To provide, operate, and maintain the future Korrith platform</li>
|
||||
<li>To generate security posture assessments for authorized domains during testing</li>
|
||||
<li>To send technical notices, updates, and administrative messages</li>
|
||||
</ul>
|
||||
|
||||
<h2>Data security</h2>
|
||||
<p>
|
||||
We are implementing robust, industry standard security measures to protect personal information and future assessment data. We are designing our systems so that data is encrypted in transit using TLS 1.3 and at rest using AES-256. Access to any gathered data is strictly controlled and limited to authorized development personnel.
|
||||
</p>
|
||||
|
||||
<h2>Your rights</h2>
|
||||
<p>
|
||||
Depending on your jurisdiction, you may have the right to request access to the personal data we currently hold about you, request corrections to inaccurate data, or request the deletion of your waitlist data. To exercise these rights, please contact our team.
|
||||
</p>
|
||||
|
||||
<h2>Contact us</h2>
|
||||
<p>
|
||||
If you have any questions about this Privacy Policy or our intended data handling practices as we build Korrith, please contact us at privacy@korrith.com.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Cpu,
|
||||
Lock,
|
||||
Database,
|
||||
Fingerprint,
|
||||
ShieldAlert,
|
||||
ListOrdered,
|
||||
ArrowRight,
|
||||
Info,
|
||||
GitBranch,
|
||||
Network
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
export default function QuantumReadinessPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Quantum Readiness | Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Korrith inventories your cryptography and shows you exactly where quantum computing will eventually break it, so your migration to quantum-safe is a plan, not a panic."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[80vh] flex flex-col justify-center pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--accent-purple)/0.12),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--accent-ai)/0.08),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
{/* Abstract Quantum Decorative Element */}
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-[600px] h-[600px] opacity-20 pointer-events-none hidden lg:block">
|
||||
<div className="absolute inset-0 border-[1px] border-accent-purple/30 rounded-full animate-[spin_60s_linear_infinite]" style={{ borderStyle: 'dashed' }}></div>
|
||||
<div className="absolute inset-8 border-[1px] border-accent-ai/30 rounded-full animate-[spin_40s_linear_infinite_reverse]" style={{ borderStyle: 'dashed' }}></div>
|
||||
<div className="absolute inset-16 border-[1px] border-primary/30 rounded-full animate-[spin_50s_linear_infinite]"></div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<Cpu className="w-4 h-4 text-accent-purple" />
|
||||
Layer 4: Quantum Readiness
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
Know where quantum breaks your cryptography.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-12">
|
||||
A sufficiently powerful quantum computer will break much of the public-key cryptography the internet runs on today. The threat is not hypothetical to plan for later: data captured now can be decrypted then, which is why the time to know your exposure is now. Korrith inventories your cryptographic posture and shows you exactly where you are exposed.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button asChild size="lg" className="h-12 px-8 shadow-[0_0_20px_-5px_hsl(var(--accent-purple)/0.4)] bg-accent-purple hover:bg-accent-purple/90 text-white">
|
||||
<Link to="/assessment">Get a free assessment</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Harvest Now, Decrypt Later */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center"
|
||||
>
|
||||
<div>
|
||||
<motion.div variants={fadeInUp} className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<Lock className="w-8 h-8 text-accent-red" />
|
||||
</motion.div>
|
||||
<motion.h2 variants={fadeInUp} className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Harvest now, decrypt later
|
||||
</motion.h2>
|
||||
<motion.p variants={fadeInUp} className="text-lg text-muted-foreground leading-relaxed mb-6">
|
||||
Adversaries are already capturing encrypted data they cannot read yet, betting they will be able to later. Anything with a long confidentiality lifetime, contracts, health records, intellectual property, is at risk from a future that arrives on someone else's timeline.
|
||||
</motion.p>
|
||||
<motion.p variants={fadeInUp} className="text-lg text-muted-foreground leading-relaxed">
|
||||
Quantum readiness is about knowing what you would lose and getting ahead of it.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<motion.div variants={fadeInUp} className="relative h-[400px] rounded-3xl bg-card border border-border overflow-hidden flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--accent-red)/0.05),transparent_70%)]"></div>
|
||||
|
||||
{/* Decorative Animation for Harvest/Decrypt */}
|
||||
<div className="relative z-10 flex flex-col items-center w-full px-8">
|
||||
<div className="flex items-center justify-between w-full max-w-sm mb-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-12 h-12 rounded-full bg-secondary border border-border flex items-center justify-center z-10">
|
||||
<Database className="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
<span className="text-xs font-mono text-muted-foreground">Encrypted Data</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-border via-accent-red/50 to-border relative">
|
||||
<motion.div
|
||||
className="absolute top-1/2 -translate-y-1/2 left-0 w-2 h-2 rounded-full bg-accent-red shadow-[0_0_10px_rgba(240,106,114,0.8)]"
|
||||
animate={{ left: ["0%", "100%", "0%"] }}
|
||||
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-12 h-12 rounded-full bg-accent-red/10 border border-accent-red/30 flex items-center justify-center z-10">
|
||||
<Lock className="w-5 h-5 text-accent-red" />
|
||||
</div>
|
||||
<span className="text-xs font-mono text-accent-red">Harvested</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-secondary/50 border border-border rounded-lg p-4 w-full max-w-sm backdrop-blur-sm">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wider">Status</span>
|
||||
<span className="text-xs text-accent-red font-mono">Awaiting Decryption</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-background rounded overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-accent-red/50"
|
||||
initial={{ width: "0%" }}
|
||||
whileInView={{ width: "45%" }}
|
||||
transition={{ duration: 2, ease: "easeOut", delay: 0.5 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What Korrith Inventories (Bento Layout) */}
|
||||
<section className="py-24 bg-secondary/20 border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div {...fadeInUp} className="mb-16 max-w-3xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
What Korrith inventories
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||||
>
|
||||
{/* Card 1: Full width on top */}
|
||||
<motion.div variants={fadeInUp} className="md:col-span-2">
|
||||
<Card className="h-full bg-card/60 border-border/50 hover:border-accent-purple/50 hover:bg-card hover:shadow-lg transition-all duration-300 flex flex-col md:flex-row items-start md:items-center gap-6 p-6 group overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-accent-purple/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"></div>
|
||||
<div className="w-16 h-16 shrink-0 rounded-2xl bg-background border border-border flex items-center justify-center group-hover:scale-110 transition-transform duration-300 shadow-sm relative z-10">
|
||||
<Fingerprint className="w-8 h-8 text-accent-purple" />
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<CardTitle className="text-2xl text-foreground font-semibold leading-tight text-balance mb-3">
|
||||
Cryptographic inventory
|
||||
</CardTitle>
|
||||
<CardDescription className="text-lg text-muted-foreground leading-relaxed max-w-3xl">
|
||||
The algorithms, key sizes, and protocols actually in use across your assets, not what a policy says should be in use.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Card 2: Half width */}
|
||||
<motion.div variants={fadeInUp}>
|
||||
<Card className="h-full bg-card/60 border-border/50 hover:border-accent-ai/50 hover:bg-card hover:shadow-lg transition-all duration-300 flex flex-col p-6 group overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-accent-ai/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"></div>
|
||||
<div className="w-14 h-14 rounded-2xl bg-background border border-border flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300 shadow-sm relative z-10">
|
||||
<Cpu className="w-7 h-7 text-accent-ai" />
|
||||
</div>
|
||||
<div className="relative z-10 mt-auto">
|
||||
<CardTitle className="text-xl text-foreground font-semibold leading-tight text-balance mb-3">
|
||||
Quantum exposure
|
||||
</CardTitle>
|
||||
<CardDescription className="text-base text-muted-foreground leading-relaxed">
|
||||
Which of those are vulnerable to quantum attacks, so you can see your real exposure rather than guess at it.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Card 3: Half width */}
|
||||
<motion.div variants={fadeInUp}>
|
||||
<Card className="h-full bg-card/60 border-border/50 hover:border-amber-500/50 hover:bg-card hover:shadow-lg transition-all duration-300 flex flex-col p-6 group overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-amber-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"></div>
|
||||
<div className="w-14 h-14 rounded-2xl bg-background border border-border flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300 shadow-sm relative z-10">
|
||||
<ShieldAlert className="w-7 h-7 text-amber-500" />
|
||||
</div>
|
||||
<div className="relative z-10 mt-auto">
|
||||
<CardTitle className="text-xl text-foreground font-semibold leading-tight text-balance mb-3">
|
||||
Prioritized risk
|
||||
</CardTitle>
|
||||
<CardDescription className="text-base text-muted-foreground leading-relaxed">
|
||||
Which exposed cryptography protects your most sensitive or longest-lived data, so you know what to migrate first.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* From Inventory to a Plan */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center"
|
||||
>
|
||||
<div className="order-2 lg:order-1">
|
||||
{/* Visual Flow Indicator */}
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{ title: "Clear Inventory", desc: "Discover active cryptographic usage", icon: ListOrdered },
|
||||
{ title: "Ranked Exposure", desc: "Identify quantum-vulnerable protocols", icon: ShieldAlert },
|
||||
{ title: "Quantum-Safe Path", desc: "Migrate based on verified priority", icon: GitBranch }
|
||||
].map((step, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
variants={fadeInUp}
|
||||
className="flex gap-6 group"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-12 h-12 rounded-full bg-secondary border border-border flex items-center justify-center group-hover:bg-primary/10 group-hover:border-primary/30 group-hover:text-primary transition-colors duration-300">
|
||||
<step.icon className="w-5 h-5 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
{idx < 2 && (
|
||||
<div className="w-px h-12 bg-border my-2"></div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-2 pb-6">
|
||||
<h3 className="text-lg font-medium text-foreground mb-1">{step.title}</h3>
|
||||
<p className="text-muted-foreground">{step.desc}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="order-1 lg:order-2 pl-0 lg:pl-8">
|
||||
<motion.div variants={fadeInUp} className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<Network className="w-8 h-8 text-primary" />
|
||||
</motion.div>
|
||||
<motion.h2 variants={fadeInUp} className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
From inventory to a plan
|
||||
</motion.h2>
|
||||
<motion.p variants={fadeInUp} className="text-lg text-muted-foreground leading-relaxed mb-6">
|
||||
A cryptographic migration is large and slow, and doing it blind is how organizations stall. Korrith turns it into a sequenced plan: a clear inventory, a ranked exposure list, and a path to quantum-safe cryptography ordered by risk.
|
||||
</motion.p>
|
||||
<motion.p variants={fadeInUp} className="text-lg text-foreground font-medium leading-relaxed">
|
||||
You move deliberately instead of reactively.
|
||||
</motion.p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How it connects & Note */}
|
||||
<section className="py-24 bg-card/40 border-t border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||
|
||||
{/* How it Connects */}
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="md:col-span-2 space-y-6"
|
||||
>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground text-balance">
|
||||
How it connects
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
This page is about seeing your exposure. The migration path itself, including specialized post-quantum remediation, is covered under Post-Quantum Readiness, where Korrith assesses and routes the cryptographic work while keeping the assurance relationship in place.
|
||||
</p>
|
||||
<Button variant="link" asChild className="p-0 h-auto text-primary text-base font-medium hover:text-primary/80">
|
||||
<Link to="/solutions/post-quantum-readiness" className="flex items-center gap-2">
|
||||
View Post-Quantum Readiness
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
{/* Note Callout */}
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="md:col-span-1"
|
||||
>
|
||||
<div className="flex gap-4 p-6 rounded-2xl bg-secondary/50 border border-border/60 h-full">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<Info className="w-5 h-5 text-accent-ai" />
|
||||
</div>
|
||||
<p className="text-base text-secondary-foreground leading-relaxed">
|
||||
<strong>Note:</strong> built and rolling out as part of the host-internal assessment.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-background border-t border-border/50 text-center">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Prepare for the quantum transition
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10">
|
||||
Get an accurate inventory of your cryptography before the threat materializes.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--accent-purple)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--accent-purple)/0.6)] bg-accent-purple hover:bg-accent-purple/90 text-white transition-all group"
|
||||
>
|
||||
<Link to="/assessment">
|
||||
Get a free assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
FileSignature,
|
||||
ShieldCheck,
|
||||
CheckCircle2,
|
||||
History,
|
||||
Users,
|
||||
BarChart,
|
||||
ArrowRight,
|
||||
Info,
|
||||
FileCheck,
|
||||
Search,
|
||||
LockKeyhole,
|
||||
Check,
|
||||
Building,
|
||||
Briefcase
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
export default function ReportsAndEvidencePage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Reports and Evidence | Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Korrith turns findings into audit-grade reports and a tamper-evident record of what was found, validated, and fixed, so you can prove your security on demand."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[80vh] flex flex-col justify-center pt-32 pb-24 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--emerald)/0.08),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<FileSignature className="w-4 h-4 text-primary" />
|
||||
Layer 3: Reports & Evidence
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
Proof, from exposure to closure.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-12">
|
||||
Most tools end at a list of problems. Korrith ends at proof: a clear, audit-grade report and a tamper-evident record of what was found, what was validated as real, and what has been verified closed. The kind of evidence you can put in front of an auditor, a customer, or a board without rehearsing excuses.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button asChild size="lg" className="h-14 px-8 text-base font-semibold shadow-[0_0_20px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_30px_-5px_hsl(var(--primary)/0.6)] transition-all">
|
||||
<Link to="/sample-report">See a sample report</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content Sections */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-32">
|
||||
|
||||
{/* 1. Validate what is real, first */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center"
|
||||
>
|
||||
<div className="order-2 lg:order-1 pr-0 lg:pr-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<Search className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Validate what is real, first
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mb-6">
|
||||
A report full of unconfirmed findings wastes everyone's time. Korrith validates exposures before they reach your report, so what you and your stakeholders see is what an attacker could actually use, with the theoretical noise filtered out.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Confirmed items are marked confirmed; unverified items are marked unverified. Nothing is dressed up.
|
||||
</p>
|
||||
</div>
|
||||
<div className="order-1 lg:order-2">
|
||||
<div className="relative h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden flex items-center justify-center group">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-sm space-y-4 p-6">
|
||||
{/* Confirmed Item */}
|
||||
<div className="bg-card border border-primary/50 rounded-xl p-5 shadow-lg transform group-hover:-translate-y-1 transition-transform duration-300 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/10 rounded-full blur-2xl -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div className="flex items-center justify-between mb-3 relative z-10">
|
||||
<span className="font-mono text-sm text-foreground">CVE-2023-44487</span>
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 bg-primary/10 text-primary rounded text-xs font-medium border border-primary/20">
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
Confirmed
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 w-3/4 bg-muted-foreground/20 rounded mb-2"></div>
|
||||
<div className="h-2 w-1/2 bg-muted-foreground/20 rounded"></div>
|
||||
</div>
|
||||
|
||||
{/* Unverified Item */}
|
||||
<div className="bg-card border border-border rounded-xl p-5 shadow-sm opacity-60 transform group-hover:translate-y-1 transition-transform duration-300">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-mono text-sm text-muted-foreground">Obsolete Header</span>
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 bg-secondary text-muted-foreground rounded text-xs font-medium border border-border">
|
||||
<Info className="w-3 h-3" />
|
||||
Unverified
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 w-2/3 bg-muted rounded mb-2"></div>
|
||||
<div className="h-2 w-1/3 bg-muted rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 2. Prove what is closed */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center"
|
||||
>
|
||||
<div>
|
||||
<div className="relative h-[400px] rounded-3xl bg-secondary/30 border border-border/50 overflow-hidden flex items-center justify-center group p-6">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--emerald)/0.08),transparent_70%)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-[280px]">
|
||||
<div className="absolute left-[19px] top-4 bottom-4 w-px bg-border group-hover:bg-emerald-500/30 transition-colors duration-500"></div>
|
||||
|
||||
<div className="space-y-6 relative">
|
||||
{[
|
||||
{ title: "First seen", date: "Oct 12, 14:00Z", icon: Search, color: "text-muted-foreground", bg: "bg-secondary" },
|
||||
{ title: "Confirmed", date: "Oct 12, 14:05Z", icon: ShieldCheck, color: "text-primary", bg: "bg-primary/10 border-primary/20" },
|
||||
{ title: "Remediated", date: "Oct 14, 09:12Z", icon: History, color: "text-amber-500", bg: "bg-amber-500/10 border-amber-500/20" },
|
||||
{ title: "Verified Closed", date: "Oct 14, 09:15Z", icon: Check, color: "text-emerald-500", bg: "bg-emerald-500/10 border-emerald-500/30", active: true }
|
||||
].map((step, idx) => (
|
||||
<div key={idx} className="flex gap-4 items-start relative">
|
||||
<div className={`w-10 h-10 shrink-0 rounded-full border ${step.bg} flex items-center justify-center bg-card relative z-10 transition-transform duration-300 ${step.active ? 'scale-110 shadow-[0_0_15px_-3px_hsl(var(--emerald)/0.4)]' : ''}`}>
|
||||
<step.icon className={`w-4 h-4 ${step.color}`} />
|
||||
</div>
|
||||
<div className={`pt-2 ${step.active ? 'transform translate-x-1 transition-transform' : ''}`}>
|
||||
<p className={`text-sm font-semibold ${step.active ? 'text-emerald-500' : 'text-foreground'}`}>{step.title}</p>
|
||||
<p className="text-xs text-muted-foreground font-mono mt-1">{step.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-0 lg:pl-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Prove what is closed
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mb-6">
|
||||
When you remediate, Korrith does not take your word for it. It re-checks, and only marks a finding resolved when a later assessment actually observed the exposure was gone.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Every finding carries its history: first seen, confirmed, remediated, and verified closed, with the evidence and dates attached. A silently reopened issue resurfaces as open, not as a stale green checkmark.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3 & 4. Audit-grade & Conversations (Grid Layout) */}
|
||||
<section className="py-24 bg-secondary/20 border-y border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12"
|
||||
>
|
||||
{/* Audit-grade by default */}
|
||||
<motion.div variants={fadeInUp} className="lg:col-span-5 flex flex-col">
|
||||
<div className="bg-card border border-border rounded-3xl p-8 lg:p-10 shadow-sm h-full flex flex-col group relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-[radial-gradient(circle_at_top_right,hsl(var(--accent-purple)/0.1),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<div className="w-14 h-14 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 relative z-10 group-hover:scale-110 transition-transform duration-300">
|
||||
<LockKeyhole className="w-7 h-7 text-accent-purple" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground mb-4 text-balance relative z-10">
|
||||
Audit-grade by default
|
||||
</h2>
|
||||
|
||||
<p className="text-base text-muted-foreground leading-relaxed mb-8 relative z-10">
|
||||
Every report is generated from the real underlying record, with a tamper-evident chain of what happened and when. You are not assembling a narrative after the fact. You are exporting the truth the platform already holds.
|
||||
</p>
|
||||
|
||||
<div className="mt-auto relative z-10 bg-secondary/50 rounded-xl p-4 border border-border flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileCheck className="w-5 h-5 text-accent-purple" />
|
||||
<span className="text-sm font-medium text-foreground">Report Hash</span>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-muted-foreground">a8f9...2c4e</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Built for conversations */}
|
||||
<motion.div variants={fadeInUp} className="lg:col-span-7 flex flex-col">
|
||||
<div className="bg-card border border-border rounded-3xl p-8 lg:p-10 shadow-sm h-full group relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-[radial-gradient(circle_at_bottom_left,hsl(var(--primary)/0.05),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-14 h-14 rounded-2xl bg-secondary flex items-center justify-center border border-border group-hover:scale-110 transition-transform duration-300">
|
||||
<Users className="w-7 h-7 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground text-balance">
|
||||
Built for the conversations that matter
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{
|
||||
icon: FileSignature,
|
||||
title: "For auditors",
|
||||
desc: "Validated findings and proof of resolution map to the evidence frameworks ask for."
|
||||
},
|
||||
{
|
||||
icon: Building,
|
||||
title: "For customers doing security due diligence",
|
||||
desc: "A credible, honest posture report instead of a self-attested checklist."
|
||||
},
|
||||
{
|
||||
icon: BarChart,
|
||||
title: "For your board",
|
||||
desc: "A clear trajectory of exposure found and exposure closed, explained by Argus."
|
||||
}
|
||||
].map((item, idx) => (
|
||||
<div key={idx} className="flex gap-4 items-start p-4 rounded-xl hover:bg-secondary/40 transition-colors border border-transparent hover:border-border/50">
|
||||
<div className="w-10 h-10 shrink-0 rounded-lg bg-background border border-border flex items-center justify-center text-primary mt-1">
|
||||
<item.icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-foreground mb-1">{item.title}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 5. Clients / Spotlight Section */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="bg-[url('https://images.unsplash.com/photo-1557683316-973673baf926?q=80&w=2829&auto=format&fit=crop')] bg-cover bg-center rounded-3xl overflow-hidden relative"
|
||||
>
|
||||
<div className="absolute inset-0 bg-background/95 backdrop-blur-sm"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background via-background/80 to-transparent"></div>
|
||||
|
||||
<div className="relative z-10 p-10 lg:p-16 max-w-3xl">
|
||||
<div className="w-16 h-16 rounded-2xl bg-secondary flex items-center justify-center border border-border mb-8 shadow-sm">
|
||||
<Briefcase className="w-8 h-8 text-foreground" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Reports your clients can show their clients
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mb-8">
|
||||
If you assess on behalf of others, the same report is something your clients can hand to their own auditors and boards, which turns your assessment into a recurring, defensible service.
|
||||
</p>
|
||||
<Button asChild variant="outline" className="bg-transparent border-border hover:bg-secondary/80">
|
||||
<Link to="/solutions/mssps">View MSSP Solutions</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Note Section */}
|
||||
<section className="py-12 bg-background border-t border-border/50">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
{...fadeInUp}
|
||||
className="flex gap-4 p-6 rounded-2xl bg-secondary/50 border border-border/60"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<Info className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<p className="text-base text-secondary-foreground leading-relaxed">
|
||||
<strong>Note:</strong> built and rolling out to the beta.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-card/20 border-t border-border/50 text-center">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
End the argument over what is real
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-10">
|
||||
Generate honest, audit-grade proof of your external posture.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/sample-report">
|
||||
See a sample report
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
|
||||
export default function ReportsPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet><title>Reports and Evidence | Korrith</title></Helmet>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-foreground">Reports and Evidence</h1>
|
||||
<p className="mt-4 text-muted-foreground text-lg">Content coming soon in Phase 2.</p>
|
||||
</div>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
|
||||
export default function ResearchPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet><title>Research and Whitepapers | Korrith</title></Helmet>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-foreground">Research and Whitepapers</h1>
|
||||
<p className="mt-4 text-muted-foreground text-lg">Content coming soon in Phase 2.</p>
|
||||
</div>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Map,
|
||||
CheckCircle2,
|
||||
Lock,
|
||||
Rocket,
|
||||
ArrowRight,
|
||||
ShieldCheck,
|
||||
CircleDot
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
const bulletVariants = {
|
||||
initial: { opacity: 0, x: -10 },
|
||||
whileInView: { opacity: 1, x: 0 },
|
||||
transition: { duration: 0.4 }
|
||||
};
|
||||
|
||||
export default function RoadmapPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Roadmap | Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Korrith is in active development and we are honest about status. Here is what is live, what is in private testing, and what is coming next."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-20 md:pt-40 md:pb-32 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,hsl(var(--primary)/0.05),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<Map className="w-4 h-4 text-primary" />
|
||||
Platform Roadmap
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
Where Korrith is, and where it is going.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-10">
|
||||
We would rather show you an honest roadmap than a fake feature list. Korrith is in active development. Here is exactly what is live, what is in private testing, and what is next, and we keep this page current as things move.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Roadmap Timeline */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="relative">
|
||||
{/* Vertical timeline line for desktop */}
|
||||
<div className="hidden lg:block absolute left-[43px] top-8 bottom-8 w-px bg-border"></div>
|
||||
|
||||
<div className="space-y-20 lg:space-y-32">
|
||||
|
||||
{/* Phase 1: Built and rolling out */}
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="relative lg:pl-32"
|
||||
>
|
||||
<div className="hidden lg:flex absolute left-0 top-0 w-[86px] h-full justify-center">
|
||||
<div className="w-12 h-12 mt-1 rounded-full bg-emerald-500/10 border border-emerald-500/30 flex items-center justify-center z-10 shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] relative">
|
||||
<div className="absolute inset-0 rounded-full border-2 border-emerald-500/20 animate-ping"></div>
|
||||
<CheckCircle2 className="w-6 h-6 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-emerald-500/20 rounded-3xl p-8 md:p-12 shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-[radial-gradient(circle_at_top_right,rgba(16,185,129,0.08),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<div className="lg:hidden w-12 h-12 rounded-full bg-emerald-500/10 border border-emerald-500/30 flex items-center justify-center mb-6 relative">
|
||||
<CheckCircle2 className="w-6 h-6 text-emerald-500" />
|
||||
</div>
|
||||
|
||||
<motion.h2 variants={fadeInUp} className="text-3xl font-bold text-foreground mb-8 flex items-center gap-4">
|
||||
Built and rolling out
|
||||
</motion.h2>
|
||||
|
||||
<div className="space-y-6 relative z-10">
|
||||
{[
|
||||
"External attack surface assessment (Layer 1). Headers, TLS and certificates, email authentication, DNS hygiene, exposed services, technology fingerprinting, and third-party script integrity, prioritized by live exploitability intelligence. In private testing to the beta.",
|
||||
"Host-internal posture (Layer 2). The read-only agent and WordPress plugin reporting signed-evidence posture across software inventory and vulnerabilities, secrets, configuration, identity and access, cryptography and quantum readiness, patching, and malware indicators.",
|
||||
"Active validation (Layer 3). Non-destructive reachability confirmation and, under signed authorization, lockout-safe credential and access validation, governed by the authorization gate and kill-switch.",
|
||||
"Cryptographic and quantum-readiness inventory.",
|
||||
"Reports and evidence, the closed assurance loop."
|
||||
].map((text, i) => (
|
||||
<motion.div key={i} variants={bulletVariants} className="flex gap-4 items-start">
|
||||
<div className="mt-1.5 w-6 h-6 shrink-0 flex items-center justify-center">
|
||||
<CircleDot className="w-4 h-4 text-emerald-500/70" />
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">{text}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Phase 2: Gated behind counsel */}
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="relative lg:pl-32"
|
||||
>
|
||||
<div className="hidden lg:flex absolute left-0 top-0 w-[86px] h-full justify-center">
|
||||
<div className="w-12 h-12 mt-1 rounded-full bg-amber-500/10 border border-amber-500/30 flex items-center justify-center z-10 shadow-[0_0_20px_-5px_rgba(245,158,11,0.2)]">
|
||||
<Lock className="w-5 h-5 text-amber-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-amber-500/20 rounded-3xl p-8 md:p-12 shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-[radial-gradient(circle_at_top_right,rgba(245,158,11,0.08),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<div className="lg:hidden w-12 h-12 rounded-full bg-amber-500/10 border border-amber-500/30 flex items-center justify-center mb-6">
|
||||
<Lock className="w-5 h-5 text-amber-500" />
|
||||
</div>
|
||||
|
||||
<motion.h2 variants={fadeInUp} className="text-3xl font-bold text-foreground mb-8">
|
||||
Gated behind counsel
|
||||
</motion.h2>
|
||||
|
||||
<div className="space-y-6 relative z-10">
|
||||
<motion.div variants={bulletVariants} className="flex gap-4 items-start bg-amber-500/5 border border-amber-500/10 rounded-2xl p-6">
|
||||
<div className="mt-1 w-6 h-6 shrink-0 flex items-center justify-center">
|
||||
<ShieldCheck className="w-5 h-5 text-amber-500" />
|
||||
</div>
|
||||
<p className="text-lg text-amber-500/90 leading-relaxed font-medium">
|
||||
Client-facing active testing. The capability is built and proven on our own assets. Pointing it at a client's assets waits on signed active-testing terms reviewed by counsel, because the authorization is the whole point.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Phase 3: On the horizon */}
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="relative lg:pl-32"
|
||||
>
|
||||
<div className="hidden lg:flex absolute left-0 top-0 w-[86px] h-full justify-center">
|
||||
<div className="w-12 h-12 mt-1 rounded-full bg-primary/10 border border-primary/30 flex items-center justify-center z-10 shadow-[0_0_20px_-5px_hsl(var(--primary)/0.3)]">
|
||||
<Rocket className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border border-border hover:border-primary/30 transition-colors duration-500 rounded-3xl p-8 md:p-12 shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-[radial-gradient(circle_at_top_right,hsl(var(--primary)/0.08),transparent_70%)] pointer-events-none opacity-50 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="lg:hidden w-12 h-12 rounded-full bg-primary/10 border border-primary/30 flex items-center justify-center mb-6">
|
||||
<Rocket className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
|
||||
<motion.h2 variants={fadeInUp} className="text-3xl font-bold text-foreground mb-8">
|
||||
On the horizon
|
||||
</motion.h2>
|
||||
|
||||
<div className="space-y-6 relative z-10">
|
||||
{[
|
||||
"Continuous validation and proof of resolution, so a reopened exposure is caught the same week.",
|
||||
"Dark-web and breach exposure monitoring, telling you when your credentials are already for sale.",
|
||||
"Deeper threat intelligence in Argus, prioritizing by what is actually being weaponized against your sector."
|
||||
].map((text, i) => (
|
||||
<motion.div key={i} variants={bulletVariants} className="flex gap-4 items-start">
|
||||
<div className="mt-1.5 w-6 h-6 shrink-0 flex items-center justify-center">
|
||||
<CircleDot className="w-4 h-4 text-primary/60 group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">{text}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Our Commitment Section */}
|
||||
<section className="py-24 bg-secondary/30 border-y border-border/50">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Our commitment
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed max-w-2xl mx-auto">
|
||||
Anything not built yet is labeled as such, here and across the product. We will not show you a result we cannot back with evidence.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-background text-center">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-card border border-border p-12 rounded-3xl relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.1),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance relative z-10">
|
||||
Help shape what we build next
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mb-10 relative z-10 max-w-xl mx-auto">
|
||||
Join the beta to get early access to live features and provide feedback on our roadmap priorities.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group relative z-10"
|
||||
>
|
||||
<Link to="/contact">
|
||||
Join the beta
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
FileText,
|
||||
ListOrdered,
|
||||
BrainCircuit,
|
||||
ShieldCheck,
|
||||
History,
|
||||
Download,
|
||||
ArrowRight,
|
||||
ShieldAlert,
|
||||
MessageSquare
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
const reportFeatures = [
|
||||
{
|
||||
icon: FileText,
|
||||
title: "An honest posture summary.",
|
||||
description: "A clear score that reflects reality, including where coverage was incomplete, never a fake-clean.",
|
||||
color: "text-blue-400",
|
||||
bg: "bg-blue-400/10 border-blue-400/20"
|
||||
},
|
||||
{
|
||||
icon: ListOrdered,
|
||||
title: "A prioritized finding list.",
|
||||
description: "Ordered by genuine risk, with the validated and the unverified clearly distinguished, so the urgent and the theoretical are never confused.",
|
||||
color: "text-rose-400",
|
||||
bg: "bg-rose-400/10 border-rose-400/20"
|
||||
},
|
||||
{
|
||||
icon: BrainCircuit,
|
||||
title: "Argus reasoning on every item.",
|
||||
description: "What the finding is, why it matters, and what to do, in language a non-specialist can act on and an engineer can implement against.",
|
||||
color: "text-purple-400",
|
||||
bg: "bg-purple-400/10 border-purple-400/20"
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "Validation status.",
|
||||
description: "Which exposures were actively confirmed as real and reachable, and which are detected but not yet validated.",
|
||||
color: "text-emerald-400",
|
||||
bg: "bg-emerald-400/10 border-emerald-400/20"
|
||||
},
|
||||
{
|
||||
icon: History,
|
||||
title: "Evidence and history.",
|
||||
description: "For each finding, when it was first seen, its current state, and, once you remediate, proof of resolution from a later check that actually observed it gone.",
|
||||
color: "text-amber-400",
|
||||
bg: "bg-amber-400/10 border-amber-400/20"
|
||||
}
|
||||
];
|
||||
|
||||
export default function SampleReportPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Sample Korrith Report | See the Depth Before You Run One</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A sample Korrith report showing the depth, the plain-language reasoning, the validation status, and the audit-grade evidence, before you run your own."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 md:pt-40 md:pb-32 overflow-hidden border-b border-border/50">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,rgba(139,92,246,0.1),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<FileText className="w-4 h-4 text-primary" />
|
||||
Sample Report
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
See exactly what you get.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-10">
|
||||
Before you run your own assessment, see what a Korrith report looks like: the depth of the findings, the plain-language reasoning from Argus, the honest scoring, and the audit-grade evidence that makes it something you can actually show.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What a Korrith report contains */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="mb-16 max-w-3xl"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
What a Korrith report contains
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8"
|
||||
>
|
||||
{/* Make the first item span full width to break grid monotony */}
|
||||
{reportFeatures.map((feature, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
variants={fadeInUp}
|
||||
className={`group relative flex flex-col sm:flex-row gap-6 bg-card hover:bg-secondary/40 border border-border rounded-3xl p-8 transition-all duration-300 ${idx === 0 ? 'md:col-span-2' : ''}`}
|
||||
>
|
||||
<div className={`w-14 h-14 shrink-0 rounded-2xl flex items-center justify-center border ${feature.bg} group-hover:scale-105 transition-transform duration-300`}>
|
||||
<feature.icon className={`w-7 h-7 ${feature.color}`} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-foreground mb-3 group-hover:text-primary transition-colors">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed text-lg">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Context Sections (Honesty & Conversation) */}
|
||||
<section className="py-24 bg-secondary/20 border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 lg:gap-24">
|
||||
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="flex flex-col"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center mb-6">
|
||||
<ShieldAlert className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-foreground mb-6 text-balance">
|
||||
Why the honesty matters in a report
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
A report you cannot trust is worse than no report. Because Korrith never marks something clean it could not confirm, and never marks something fixed it did not re-check, the document holds up under the scrutiny of an auditor, a customer, or a board. You are exporting the truth, not a sales sheet.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="flex flex-col"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-full bg-purple-500/10 border border-purple-500/20 flex items-center justify-center mb-6">
|
||||
<MessageSquare className="w-6 h-6 text-purple-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-foreground mb-6 text-balance">
|
||||
Use it to start the conversation
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
If you assess for clients, the sample report shows them exactly what they will receive, which makes the value obvious before any access is granted.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-card border border-border p-12 md:p-16 rounded-3xl relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--primary)/0.1),transparent_70%)] pointer-events-none"></div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance relative z-10">
|
||||
Ready to see the difference?
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mt-10 relative z-10">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto h-14 px-8 text-base font-semibold border-border hover:bg-secondary transition-all"
|
||||
>
|
||||
<a href="#" aria-label="Download the sample report">
|
||||
<Download className="mr-2 h-5 w-5" />
|
||||
Download the sample report
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="w-full sm:w-auto h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group"
|
||||
>
|
||||
<Link to="/assessment">
|
||||
Get your own assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Terms and Conditions | Korrith</title>
|
||||
<meta name="description" content="Korrith terms and conditions. Review the terms governing your use of our website and future platform services." />
|
||||
</Helmet>
|
||||
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
||||
<div className="mb-12">
|
||||
<h1 className="text-4xl font-bold text-foreground mb-4">Terms and Conditions</h1>
|
||||
<p className="text-muted-foreground font-mono text-sm">Last updated: 2026-06-16</p>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-invert prose-p:text-muted-foreground prose-h2:text-foreground prose-h2:mt-12 prose-h2:mb-4 prose-h2:text-2xl prose-h2:font-semibold max-w-none">
|
||||
<h2>Acceptance of terms</h2>
|
||||
<p>
|
||||
By accessing the website at korrith.com and utilizing any currently available Korrith services or waitlists, you are agreeing to be bound by these terms of service, all applicable laws and regulations, and agree that you are responsible for compliance with any applicable local laws. If you do not agree with any of these terms, you are prohibited from using or accessing this site.
|
||||
</p>
|
||||
|
||||
<h2>Development stage acknowledgment</h2>
|
||||
<p>
|
||||
You acknowledge that the Korrith platform is currently in active development. Any features, timelines, or capabilities described on the website represent our development goals and are subject to change without notice. Participation in any beta program is at your own discretion and risk.
|
||||
</p>
|
||||
|
||||
<h2>Use license</h2>
|
||||
<p>
|
||||
Permission is granted to temporarily download one copy of the materials (information or software) on Korrith's website for personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer of title.
|
||||
</p>
|
||||
<p>
|
||||
Under this license you may not:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Modify or copy the materials</li>
|
||||
<li>Attempt to use any future platform features to scan domains, networks, or infrastructure that you do not own or possess explicit authorization to test</li>
|
||||
<li>Attempt to decompile or reverse engineer any software contained on the Korrith website or platform</li>
|
||||
<li>Remove any copyright or other proprietary notations from the materials</li>
|
||||
</ul>
|
||||
|
||||
<h2>Disclaimer</h2>
|
||||
<p>
|
||||
The materials on Korrith's website and any pre-release versions of the Korrith platform are provided on an 'as is' basis. Korrith makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.
|
||||
</p>
|
||||
|
||||
<h2>Limitations of liability</h2>
|
||||
<p>
|
||||
In no event shall Korrith or its developers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on Korrith's website or early-access platform tools, even if Korrith or a Korrith authorized representative has been notified orally or in writing of the possibility of such damage.
|
||||
</p>
|
||||
|
||||
<h2>Accuracy of materials</h2>
|
||||
<p>
|
||||
The materials appearing on Korrith's website could include technical, typographical, or photographic errors. Korrith does not warrant that any of the materials on its website are accurate, complete or current. Korrith may make changes to the materials contained on its website at any time without notice.
|
||||
</p>
|
||||
|
||||
<h2>Modifications</h2>
|
||||
<p>
|
||||
Korrith may revise these terms of service for its website at any time without notice. By using this website you are agreeing to be bound by the then current version of these terms of service.
|
||||
</p>
|
||||
|
||||
<h2>Governing law</h2>
|
||||
<p>
|
||||
These terms and conditions are governed by and construed in accordance with the laws of the applicable jurisdiction and you irrevocably submit to the exclusive jurisdiction of the courts in that location.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
ShieldCheck,
|
||||
Database,
|
||||
BrainCircuit,
|
||||
Activity,
|
||||
Mail,
|
||||
Eye,
|
||||
ArrowRight
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
export default function TrustPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Trust at Korrith | How We Secure Ourselves and Your Data</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="How Korrith handles your data, constrains its own AI, and keeps active testing safe. The standard we apply to you, we apply to ourselves."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 md:pt-40 md:pb-32 overflow-hidden border-b border-border/50 min-h-[70vh] flex items-center">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<ShieldCheck className="w-4 h-4 text-primary" />
|
||||
Trust & Transparency
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
We hold ourselves to what we assess.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-2xl mb-10">
|
||||
A security company that cannot account for its own security has no standing to assess yours. So the discipline we apply to you, we apply to ourselves, and we are specific about it here.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Principles Grid Section */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8"
|
||||
>
|
||||
{/* 1. Data Handling */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-card border border-border p-8 md:p-10 rounded-3xl hover:bg-secondary/30 transition-colors flex flex-col group"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center mb-8 group-hover:scale-105 transition-transform duration-300">
|
||||
<Database className="w-7 h-7 text-blue-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">How we handle your data</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed flex-grow">
|
||||
Korrith collects the posture and findings needed to assess and assure, and no more. Sensitive values are minimized. Confirmed credentials are never stored or displayed in plaintext, only a masked, fingerprinted record is kept. End-user personal data is deliberately kept out of the inventories we surface. We collect to assess, not to accumulate.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* 2. AI Constraints */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-secondary/30 border border-border p-8 md:p-10 rounded-3xl hover:bg-secondary/50 transition-colors flex flex-col group"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center mb-8 group-hover:scale-105 transition-transform duration-300">
|
||||
<BrainCircuit className="w-7 h-7 text-purple-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">How we constrain our own AI</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed flex-grow">
|
||||
Argus is constrained to reason only over real findings, and every insight is tied to verifiable evidence. It cannot invent risk, it cannot cross client boundaries, and it shows unverified results as unverified. The honesty is built into the system, not left to good intentions.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* 3. Active Testing Safe (Full Width for Emphasis) */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="md:col-span-2 bg-card border border-border p-8 md:p-12 rounded-3xl hover:bg-secondary/20 transition-colors flex flex-col md:flex-row gap-8 lg:gap-12 items-start md:items-center relative overflow-hidden group"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_right,hsl(160,61%,53%,0.05),transparent_60%)] pointer-events-none"></div>
|
||||
|
||||
<div className="w-16 h-16 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-300 relative z-10">
|
||||
<Activity className="w-8 h-8 text-emerald-400" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex-1">
|
||||
<h2 className="text-2xl md:text-3xl font-semibold text-foreground mb-4">How we keep active testing safe</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed max-w-4xl">
|
||||
Any active testing runs only inside cryptographically signed authorization, scoped to named targets and test types, in approved windows, on owned or signed-scope assets, staging first, with a global kill-switch that halts everything instantly. It is designed to do no harm, including never locking out accounts. The engine has no path to act outside its authorization.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 4. Responsible Disclosure */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-background border border-border p-8 md:p-10 rounded-3xl hover:bg-secondary/30 transition-colors flex flex-col group"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center mb-6 group-hover:scale-105 transition-transform duration-300">
|
||||
<Mail className="w-6 h-6 text-amber-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-3">Responsible disclosure</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed mb-6">
|
||||
We welcome reports of any security concern in Korrith itself, and we will respond.
|
||||
</p>
|
||||
<div className="mt-auto">
|
||||
<a href="mailto:security@korrith.com" className="text-primary hover:text-primary/80 font-medium inline-flex items-center transition-colors">
|
||||
security@korrith.com
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 5. Honest Status */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-background border border-border p-8 md:p-10 rounded-3xl hover:bg-secondary/30 transition-colors flex flex-col group"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-rose-500/10 border border-rose-500/20 flex items-center justify-center mb-6 group-hover:scale-105 transition-transform duration-300">
|
||||
<Eye className="w-6 h-6 text-rose-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-3">Honest status</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed">
|
||||
Korrith is in active development, and we say so. We will not present a capability as live before it is, or a result as clean before we can prove it.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 bg-secondary/10">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance">
|
||||
Have more questions?
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-10 max-w-2xl mx-auto">
|
||||
We believe in open communication about our architecture, capabilities, and roadmap. If there's something we haven't covered here, our FAQ goes deeper into how the platform works.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group">
|
||||
<Link to="/faq">
|
||||
Read the FAQ
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Globe,
|
||||
Database,
|
||||
FileText,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
ArrowRight,
|
||||
ShieldAlert
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.6, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
export default function WebsiteSecurityAuditPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Website Security Audits | Korrith</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Korrith audits your website's external and internal posture and tells you, in plain language, exactly what is exposed and what to fix. No risk to your site."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 md:pt-40 md:pb-32 overflow-hidden border-b border-border/50 min-h-[85vh] flex items-center">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,rgba(155,77,255,0.1),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full text-center flex flex-col items-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl flex flex-col items-center"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-secondary/80 border border-border backdrop-blur-sm text-sm font-medium text-secondary-foreground mb-8">
|
||||
<ShieldAlert className="w-4 h-4 text-primary" />
|
||||
Website Security Audits
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
Know whether your website is actually safe.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl mb-10 text-balance">
|
||||
Most website owners genuinely do not know if their site is secure, and the tools that claim to tell them produce either a meaningless green badge or an unreadable wall of warnings. Korrith audits your site the way an adversary would assess it, then tells you in plain language what is exposed and what to do about it.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" className="h-14 px-8 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group">
|
||||
<Link to="/assessment">
|
||||
Get a free assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Two Levels of Audit */}
|
||||
<section className="py-24 bg-background border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-6 text-balance">
|
||||
Two levels of audit
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||
>
|
||||
{/* External Audit Card */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-card border border-border p-8 md:p-12 rounded-3xl hover:bg-secondary/40 transition-colors relative overflow-hidden group"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 rounded-full blur-3xl pointer-events-none transition-opacity group-hover:bg-blue-500/10"></div>
|
||||
|
||||
<div className="w-14 h-14 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center mb-8 relative z-10 group-hover:scale-105 transition-transform duration-300">
|
||||
<Globe className="w-7 h-7 text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-foreground mb-4 relative z-10">External audit, no install.</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed relative z-10">
|
||||
Verify your domain and Korrith immediately assesses your site's internet-facing surface: headers, TLS and certificates, email authentication, DNS, exposed services, the technologies you run and whether they are outdated, and the integrity of third-party scripts. Nothing is installed and nothing is changed.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Deeper Audit Card */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="bg-secondary/30 border border-border p-8 md:p-12 rounded-3xl hover:bg-secondary/50 transition-colors relative overflow-hidden group"
|
||||
>
|
||||
<div className="absolute bottom-0 right-0 w-64 h-64 bg-purple-500/5 rounded-full blur-3xl pointer-events-none transition-opacity group-hover:bg-purple-500/10"></div>
|
||||
|
||||
<div className="w-14 h-14 rounded-2xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center mb-8 relative z-10 group-hover:scale-105 transition-transform duration-300">
|
||||
<Database className="w-7 h-7 text-purple-400" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-foreground mb-4 relative z-10">Deeper audit, read-only.</h3>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed relative z-10">
|
||||
For supported platforms, including WordPress, a read-only, outbound-only plugin reports your site's internal posture as cryptographically signed evidence: components and known vulnerabilities, configuration, identity and access, and file integrity. It never modifies your site, never exfiltrates data, and installs only with your explicit authorization.
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What You Actually Receive (Spotlight Layout) */}
|
||||
<section className="py-32 bg-secondary/10 border-b border-border/50">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<div className="w-20 h-20 rounded-3xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center mb-10 shadow-[0_0_30px_-5px_hsl(160,61%,53%,0.2)]">
|
||||
<FileText className="w-10 h-10 text-emerald-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-8 text-balance">
|
||||
What you actually receive
|
||||
</h2>
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed text-balance">
|
||||
A prioritized, plain-language report that says what is wrong, why it matters, and what to do, with the genuinely urgent items separated from the merely tidy. Argus writes the explanation, so you do not need a security background to act on it.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Grid: Honest Every Time & Perfect For Agencies */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-12 lg:gap-16"
|
||||
>
|
||||
<motion.div variants={fadeInUp} className="flex flex-col">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-12 h-12 rounded-xl bg-card border border-border flex items-center justify-center shrink-0 shadow-sm">
|
||||
<ShieldCheck className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground">
|
||||
Honest, every time
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
If a check could not run, the report says so. You will never get a clean bill of health that was really just a failed scan. That honesty is the difference between a report you can trust and a badge you cannot.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp} className="flex flex-col">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-12 h-12 rounded-xl bg-card border border-border flex items-center justify-center shrink-0 shadow-sm">
|
||||
<Users className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-foreground">
|
||||
Perfect for agencies and owners alike
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Run it on your own site, or, if you manage sites for others, run it across all of them with per-client separation and reporting you can deliver.
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
ArrowRight,
|
||||
BrainCircuit,
|
||||
Fingerprint,
|
||||
RefreshCcw,
|
||||
ShieldCheck,
|
||||
Crosshair,
|
||||
Sparkles
|
||||
} from 'lucide-react';
|
||||
import LayoutWrapper from '@/components/LayoutWrapper.jsx';
|
||||
import { Button } from '@/components/ui/button.jsx';
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 30 },
|
||||
whileInView: { opacity: 1, y: 0 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { duration: 0.7, ease: "easeOut" }
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
initial: { opacity: 0 },
|
||||
whileInView: { opacity: 1 },
|
||||
viewport: { once: true, margin: "-50px" },
|
||||
transition: { staggerChildren: 0.15 }
|
||||
};
|
||||
|
||||
const principles = [
|
||||
{
|
||||
title: "Grounded AI: no invented risk",
|
||||
body: "AI in security usually means a black box that produces a confident paragraph you cannot check. We built Argus the opposite way. Every insight Argus produces is tied to a specific, verifiable finding through a grounding mechanism, so each claim carries its evidence with it. If Argus cannot point to a real finding behind a statement, it does not make the statement. The practical result: you can trust the priorities, because none of them are hallucinated, and you can click any insight and land on the exact finding that produced it.",
|
||||
icon: BrainCircuit,
|
||||
color: "text-purple-400",
|
||||
bg: "bg-purple-500/10",
|
||||
border: "border-purple-500/20"
|
||||
},
|
||||
{
|
||||
title: "Provable authorization: the engine cannot act outside its scope",
|
||||
body: "The line between an assurance product and a hacking tool is authorization. Most tools treat that line as a policy in a document. Korrith treats it as code. Active testing passes through a single gate that fails closed: it runs only when a current, cryptographically signed authorization covers that exact target and test type, in an approved window, on the agreed environment, with the kill-switch up. Anything else is denied, with a reason. There is no path for the engine to act outside the authorization you grant, and a single control halts everything instantly. This is why we can offer active validation responsibly.",
|
||||
icon: Fingerprint,
|
||||
color: "text-blue-400",
|
||||
bg: "bg-blue-500/10",
|
||||
border: "border-blue-500/20"
|
||||
},
|
||||
{
|
||||
title: "The closed loop, with proof of resolution",
|
||||
body: "Finding a problem is the easy half. Proving it is gone is the half that matters at audit time. Korrith tracks every finding through its whole life: first seen, validated as real, remediated, and verified closed by a later check that actually observed its absence. A fix is not marked done because someone clicked a button. It is shown done because Korrith looked again and the exposure was no longer there. That record is continuous, so a regression that quietly reopens is caught the same week, not at the next annual review.",
|
||||
icon: RefreshCcw,
|
||||
color: "text-emerald-400",
|
||||
bg: "bg-emerald-500/10",
|
||||
border: "border-emerald-500/20"
|
||||
},
|
||||
{
|
||||
title: "Honest by design: never fake-clean",
|
||||
body: "A security tool that reports clean when it simply could not check is worse than useless, because it sells you false confidence. Korrith reports coverage exactly as it is. Where a check could not run or a data source was unavailable, the product says so. Unverified is shown as unverified. That discipline runs through the scoring, the dashboards, and the reports, and it is the reason a Korrith report is something you can put in front of an auditor or a board without flinching.",
|
||||
icon: ShieldCheck,
|
||||
color: "text-amber-400",
|
||||
bg: "bg-amber-500/10",
|
||||
border: "border-amber-500/20"
|
||||
},
|
||||
{
|
||||
title: "Built for the threat as it is now",
|
||||
body: "Attackers use AI to find and exploit faster, and they shop a dark-web economy of stolen credentials and ready footholds. Korrith is built to meet that: external first, the way an adversary maps you, with reasoning that prioritizes by what is actually being weaponized, and a clear path to quantum-safe cryptography as that threat matures.",
|
||||
icon: Crosshair,
|
||||
color: "text-rose-400",
|
||||
bg: "bg-rose-500/10",
|
||||
border: "border-rose-500/20"
|
||||
},
|
||||
{
|
||||
title: "What this means for you",
|
||||
body: "You stop drowning in findings and start working from a short, reasoned, evidence-backed list. You can prove your security on demand. And you can trust what the tool tells you, because it refuses to tell you anything it cannot back.",
|
||||
icon: Sparkles,
|
||||
color: "text-primary",
|
||||
bg: "bg-primary/10",
|
||||
border: "border-primary/20"
|
||||
}
|
||||
];
|
||||
|
||||
export default function WhyKorrithPage() {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<Helmet>
|
||||
<title>Why Korrith | Reasoned, Proven, Honest Security</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Plenty of tools find problems. Korrith proves they are real, proves they are fixed, and shows its work. Here is exactly how, and why it matters."
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 md:pt-48 md:pb-32 overflow-hidden border-b border-border/50 min-h-[80vh] flex items-center">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,hsl(var(--primary)/0.15),transparent_50%)] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,rgba(155,77,255,0.1),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full text-center flex flex-col items-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="max-w-4xl flex flex-col items-center"
|
||||
>
|
||||
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground mb-8 text-balance leading-[1.1]">
|
||||
Most tools find problems.<br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-purple-400">
|
||||
Korrith proves them.
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground leading-relaxed max-w-3xl text-balance">
|
||||
Korrith was built to close the gap between a finding and the truth: is it real, and is it actually fixed. Everything below is a design decision we made on purpose, because security you cannot prove is just a feeling.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Core Principles (Numbered List Layout) */}
|
||||
<section className="py-24 md:py-32 bg-background relative overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="flex flex-col gap-20 md:gap-32">
|
||||
{principles.map((principle, index) => {
|
||||
const Icon = principle.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={fadeInUp}
|
||||
initial="initial"
|
||||
whileInView="whileInView"
|
||||
className="flex flex-col md:flex-row gap-8 lg:gap-16 items-start group"
|
||||
>
|
||||
{/* Decorative Number */}
|
||||
<div className="hidden md:block w-32 shrink-0">
|
||||
<div className="text-7xl lg:text-8xl font-black text-foreground/[0.03] select-none font-mono leading-none tracking-tighter group-hover:text-primary/10 transition-colors duration-500">
|
||||
{(index + 1).toString().padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Card */}
|
||||
<div className="flex-1 bg-card border border-border p-8 md:p-12 rounded-3xl hover:bg-secondary/40 transition-colors relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full blur-3xl pointer-events-none transition-opacity group-hover:bg-primary/10"></div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-8 relative z-10">
|
||||
<div className="md:hidden text-4xl font-black text-foreground/[0.05] select-none font-mono leading-none tracking-tighter mr-2">
|
||||
{(index + 1).toString().padStart(2, '0')}
|
||||
</div>
|
||||
<div className={`w-14 h-14 rounded-2xl ${principle.bg} border ${principle.border} flex items-center justify-center shrink-0`}>
|
||||
<Icon className={`w-7 h-7 ${principle.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-6 text-balance relative z-10">
|
||||
{principle.title}
|
||||
</h2>
|
||||
|
||||
<p className="text-xl text-muted-foreground leading-relaxed relative z-10 max-w-[65ch]">
|
||||
{principle.body}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-32 bg-secondary/20 border-t border-border/50 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,hsl(var(--primary)/0.1),transparent_50%)] pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-foreground mb-6 text-balance">
|
||||
Stop guessing. Start proving.
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-10 text-balance max-w-2xl mx-auto">
|
||||
Experience the platform built to prioritize truth over noise, and evidence over assumptions.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" className="h-14 px-10 text-base font-semibold shadow-[0_0_30px_-5px_hsl(var(--primary)/0.4)] hover:shadow-[0_0_40px_-5px_hsl(var(--primary)/0.6)] transition-all group">
|
||||
<Link to="/assessment">
|
||||
Get a free assessment
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</LayoutWrapper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user