Initial Phoenix Sankofa Cloud setup

- Complete project structure with Next.js frontend
- GraphQL API backend with Apollo Server
- Portal application with NextAuth
- Crossplane Proxmox provider
- GitOps configurations
- CI/CD pipelines
- Testing infrastructure (Vitest, Jest, Go tests)
- Error handling and monitoring
- Security hardening
- UI component library
- Documentation
This commit is contained in:
defiQUG
2025-11-28 12:54:33 -08:00
commit 6f28146ac3
229 changed files with 43136 additions and 0 deletions

140
src/app/about/page.tsx Normal file
View File

@@ -0,0 +1,140 @@
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export default function AboutPage() {
return (
<main className="min-h-screen bg-studio-black">
{/* Header */}
<header className="border-b border-studio-medium bg-studio-dark">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<Link href="/" className="text-2xl font-bold text-white">
Phoenix <span className="text-phoenix-fire">Sankofa</span> Cloud
</Link>
<nav className="flex gap-4">
<Link href="/" className="text-gray-400 hover:text-white">
Home
</Link>
<Link href="/products" className="text-gray-400 hover:text-white">
Products
</Link>
<Link href="/manifesto" className="text-gray-400 hover:text-white">
Manifesto
</Link>
</nav>
</div>
</header>
{/* Hero */}
<section className="py-24 px-4">
<div className="mx-auto max-w-4xl text-center">
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
The Deeper Meaning
</h1>
<p className="text-xl text-gray-300">
Understanding the Akan philosophy of Sankofa and the Phoenix transformation
</p>
</div>
</section>
{/* Philosophy Section */}
<section className="py-16 px-4">
<div className="mx-auto max-w-4xl space-y-12">
<Card>
<CardHeader>
<CardTitle className="text-phoenix-fire">Sankofa and Akan Cosmology</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-gray-300">
<p>
In Akan cosmology, the universe is circular, not linear; interconnected, not hierarchical;
ancestrally governed, not isolated. Sankofa represents the return to origin to complete the cycle.
</p>
<p>
It echoes the Akan belief that life, identity, and destiny (nkrabea) must be aligned with
source (kra), ancestral memory (nananom nsamanfoɔ), and lineage (abusua).
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sankofa-gold">Sankofa and Time</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-gray-300">
<p>
Western time is linear. Akan time is recursive events are patterns that return.
</p>
<p className="text-lg font-semibold text-white">
Progress is a spiral, not a straight line.
</p>
<p>
The future is found in the past; the past informs the future. This is why a bird looking
back while flying forward is perfect as the symbol.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-neon-cyan">Sankofa and the Soul</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-gray-300">
<p>
In Akan thought, "Kra" is the divine soul, given by Nyame (Creator). It comes from a
spiritual origin place, and its mission is learned by reflecting on where it came from.
</p>
<p className="text-lg font-semibold text-white">
Sankofa is the soul's act of remembering itself.
</p>
<p>It is spiritual self-retrieval.</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-phoenix-fire">Sankofa as Technology</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-gray-300">
<p>
Sankofa is an algorithm. A cosmological protocol. A recursive pattern of:
</p>
<p className="text-2xl font-bold text-center text-white">
Remember Retrieve Restore Rise
</p>
<p>
Sankofa is how individuals, families, nations, and civilizations rebuild themselves.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sankofa-gold">The PhoenixSankofa Integration</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-gray-300">
<p>
<strong className="text-white">Phoenix</strong> = fire, rebirth, immortality
</p>
<p>
<strong className="text-white">Sankofa</strong> = return, reclaim, rise forward
</p>
<p className="text-xl font-semibold text-center text-white mt-6">
Together, they form: <span className="text-phoenix-fire">Rebirth + Ancestral Return = Sovereign Global Power</span>
</p>
</CardContent>
</Card>
</div>
</section>
{/* CTA */}
<section className="py-16 px-4">
<div className="mx-auto max-w-4xl text-center">
<Link href="/manifesto">
<Button variant="phoenix" size="lg">Read the Full Manifesto</Button>
</Link>
</div>
</section>
</main>
)
}

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

@@ -0,0 +1,60 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 10 10 10;
--foreground: 255 255 255;
}
* {
@apply border-studio-medium;
}
body {
@apply bg-studio-black text-foreground;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.glow-phoenix {
box-shadow: 0 0 20px rgba(255, 69, 0, 0.5);
}
.glow-sankofa {
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
}
.glow-neon {
box-shadow: 0 0 20px rgba(0, 255, 209, 0.5);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
padding: inherit;
margin: inherit;
overflow: visible;
clip: auto;
white-space: normal;
}
}

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

@@ -0,0 +1,32 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { Providers } from './providers'
import { ErrorBoundary } from '@/components/error-boundary'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
export const metadata: Metadata = {
title: 'Phoenix Sankofa Cloud',
description: 'The sovereign cloud born of fire and ancestral wisdom.',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="dark">
<body className={`${inter.variable} font-sans antialiased`}>
<ErrorBoundary>
<Providers>{children}</Providers>
</ErrorBoundary>
</body>
</html>
)
}

167
src/app/manifesto/page.tsx Normal file
View File

@@ -0,0 +1,167 @@
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export default function ManifestoPage() {
const principles = [
{
title: 'Sovereignty',
description: 'Infrastructure must serve sovereignty, not subvert it. Every region, nation, and community has the right to control its own data, govern its own infrastructure, and determine its own technological destiny.',
},
{
title: 'Ancestral Wisdom',
description: 'Technology must remember its origins. The cloud must learn from the past, honor ancestral knowledge, and integrate traditional wisdom with modern innovation.',
},
{
title: 'Identity',
description: 'Infrastructure must reflect identity. Technology is not neutral. We build infrastructure that reflects our cultural identity, honors our heritage, and serves our communities.',
},
{
title: 'Recursive Learning',
description: 'Progress is a spiral, not a straight line. We build systems that remember what came before, learn from history, and return to origin to complete cycles.',
},
{
title: 'Global Cultural Intelligence',
description: 'The cloud must understand culture. Across 325 regions, we build infrastructure that respects cultural contexts, adapts to local needs, and honors diverse traditions.',
},
{
title: 'Rebirth and Transformation',
description: 'Like the Phoenix, we rise from fire. We build infrastructure that transforms continuously, renews itself, and emerges stronger from every cycle.',
},
]
return (
<main className="min-h-screen bg-studio-black">
{/* Header */}
<header className="border-b border-studio-medium bg-studio-dark">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<Link href="/" className="text-2xl font-bold text-white">
Phoenix <span className="text-phoenix-fire">Sankofa</span> Cloud
</Link>
<nav className="flex gap-4">
<Link href="/" className="text-gray-400 hover:text-white">
Home
</Link>
<Link href="/about" className="text-gray-400 hover:text-white">
About
</Link>
<Link href="/products" className="text-gray-400 hover:text-white">
Products
</Link>
</nav>
</div>
</header>
{/* Hero */}
<section className="py-24 px-4">
<div className="mx-auto max-w-4xl text-center">
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
The Sovereign Cloud Manifesto
</h1>
<p className="text-xl text-gray-300">
A declaration of technological sovereignty
</p>
</div>
</section>
{/* Declaration */}
<section className="py-16 px-4">
<div className="mx-auto max-w-4xl">
<Card className="border-phoenix-fire/50 bg-gradient-to-br from-phoenix-fire/10 to-sankofa-gold/10">
<CardContent className="pt-6">
<div className="space-y-4 text-lg text-white">
<p className="text-2xl font-bold">We declare the right to technological sovereignty.</p>
<p className="text-2xl font-bold">We declare the right to infrastructure that reflects our identity.</p>
<p className="text-2xl font-bold">We declare the right to cloud computing that honors our ancestors.</p>
<p className="text-2xl font-bold">We declare the right to AI that remembers where it came from.</p>
</div>
</CardContent>
</Card>
</div>
</section>
{/* Principles */}
<section className="py-16 px-4">
<div className="mx-auto max-w-6xl">
<h2 className="mb-12 text-center text-4xl font-bold text-white">
Principles
</h2>
<div className="grid gap-8 md:grid-cols-2">
{principles.map((principle) => (
<Card key={principle.title}>
<CardHeader>
<CardTitle className="text-phoenix-fire">{principle.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-300">{principle.description}</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
{/* The Promise */}
<section className="py-16 px-4">
<div className="mx-auto max-w-4xl">
<Card>
<CardHeader>
<CardTitle className="text-3xl text-center text-sankofa-gold">The Promise</CardTitle>
</CardHeader>
<CardContent className="space-y-6 pt-6">
<p className="text-center text-2xl font-bold text-white">
We promise infrastructure that:
</p>
<div className="space-y-4 text-lg text-gray-300">
<p className="flex items-center gap-3">
<span className="text-phoenix-fire text-2xl"></span>
<strong className="text-white">Remembers</strong> its origins
</p>
<p className="flex items-center gap-3">
<span className="text-phoenix-fire text-2xl"></span>
<strong className="text-white">Retrieves</strong> ancestral wisdom
</p>
<p className="flex items-center gap-3">
<span className="text-phoenix-fire text-2xl"></span>
<strong className="text-white">Restores</strong> identity and sovereignty
</p>
<p className="flex items-center gap-3">
<span className="text-phoenix-fire text-2xl"></span>
<strong className="text-white">Rises</strong> forward with purpose
</p>
</div>
<p className="mt-8 text-center text-3xl font-bold text-white">
Remember Retrieve Restore Rise
</p>
<p className="text-center text-gray-400">
This is the Sankofa cycle. This is the Phoenix transformation.
</p>
<p className="text-center text-2xl font-bold text-phoenix-fire">
This is Phoenix Sankofa Cloud.
</p>
</CardContent>
</Card>
</div>
</section>
{/* CTA */}
<section className="py-16 px-4">
<div className="mx-auto max-w-4xl text-center">
<h2 className="mb-6 text-3xl font-bold text-white">
Join the Movement
</h2>
<p className="mb-8 text-xl text-gray-400">
If you believe in technological sovereignty, ancestral wisdom, and identity-based infrastructure,
</p>
<p className="mb-8 text-xl font-semibold text-white">
Join us. Build with us. Rise with us.
</p>
<Link href="/">
<Button variant="phoenix" size="lg">Get Started</Button>
</Link>
</div>
</section>
</main>
)
}

34
src/app/page.test.tsx Normal file
View File

@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@/lib/test-utils'
import Home from './page'
describe('Home Page', () => {
it('should render the main heading', () => {
render(<Home />)
expect(screen.getByRole('heading', { level: 1, name: /phoenix.*sankofa.*cloud/i })).toBeInTheDocument()
})
it('should render the tagline', () => {
render(<Home />)
expect(screen.getByText(/the sovereign cloud born of fire and ancestral wisdom/i)).toBeInTheDocument()
})
it('should render the Remember → Retrieve → Restore → Rise text', () => {
render(<Home />)
expect(screen.getByText(/remember.*retrieve.*restore.*rise/i)).toBeInTheDocument()
})
it('should render navigation links', () => {
render(<Home />)
expect(screen.getByRole('link', { name: /learn more/i })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /products/i })).toBeInTheDocument()
})
it('should render feature cards', () => {
render(<Home />)
expect(screen.getByText(/phoenix fire/i)).toBeInTheDocument()
expect(screen.getByText(/sankofa memory/i)).toBeInTheDocument()
expect(screen.getByText(/sovereign power/i)).toBeInTheDocument()
})
})

110
src/app/page.tsx Normal file
View File

@@ -0,0 +1,110 @@
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
export default function Home() {
return (
<main className="min-h-screen bg-studio-black">
{/* Hero Section */}
<section className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4">
<div className="absolute inset-0 bg-gradient-to-br from-phoenix-fire/20 via-transparent to-sankofa-gold/20" />
<div className="relative z-10 max-w-4xl text-center">
<h1 className="mb-6 text-5xl font-bold tracking-tight text-white md:text-7xl">
Phoenix{' '}
<span className="bg-gradient-to-r from-phoenix-fire to-sankofa-gold bg-clip-text text-transparent">
Sankofa
</span>{' '}
Cloud
</h1>
<p className="mb-8 text-xl text-gray-300 md:text-2xl">
The sovereign cloud born of fire and ancestral wisdom.
</p>
<p className="mb-12 text-lg text-gray-400">
Remember Retrieve Restore Rise
</p>
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
<Link href="/about">
<Button variant="phoenix" size="lg">Learn More</Button>
</Link>
<Link href="/products">
<Button variant="outline" size="lg">Products</Button>
</Link>
</div>
</div>
</section>
{/* Features Section */}
<section className="py-24 px-4">
<div className="mx-auto max-w-6xl">
<h2 className="mb-12 text-center text-4xl font-bold text-white">
Sovereign. Ancestral. Transformative.
</h2>
<div className="grid gap-8 md:grid-cols-3">
<Card>
<CardHeader>
<CardTitle className="text-phoenix-fire">Phoenix Fire</CardTitle>
<CardDescription>
Rebirth through transformation
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-gray-300">
Infrastructure that continuously transforms and renews, rising from every challenge like the Phoenix.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sankofa-gold">Sankofa Memory</CardTitle>
<CardDescription>
Return to origin, rise forward
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-gray-300">
Systems that remember their origins, retrieve ancestral wisdom, and rise forward with purpose.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-neon-cyan">Sovereign Power</CardTitle>
<CardDescription>
True technological sovereignty
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-gray-300">
Complete control over infrastructure, data, and destiny across 325 global regions.
</p>
</CardContent>
</Card>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-24 px-4">
<div className="mx-auto max-w-4xl text-center">
<h2 className="mb-6 text-4xl font-bold text-white">
Join the Sovereign Cloud Movement
</h2>
<p className="mb-8 text-xl text-gray-400">
Build infrastructure that honors identity and serves sovereignty.
</p>
<Link href="/manifesto">
<Button variant="phoenix" size="lg">Read the Manifesto</Button>
</Link>
</div>
</section>
</main>
)
}

134
src/app/products/page.tsx Normal file
View File

@@ -0,0 +1,134 @@
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
export default function ProductsPage() {
const products = [
{
category: 'Compute',
items: [
{ name: 'PhoenixCore Compute', description: 'Core compute engine powered by Phoenix fire' },
{ name: 'SankofaEdge Nodes', description: 'Edge nodes that remember and return data' },
{ name: 'AkanFire VM Engine', description: 'High-performance VMs with Akan heritage' },
],
},
{
category: 'Storage',
items: [
{ name: 'OkraVault Storage', description: 'Storage for the soul of your data' },
{ name: 'Nananom Archive', description: 'Long-term archival with ancestral memory' },
{ name: 'Egg of the Phoenix Object Store', description: 'Object storage for transformation' },
],
},
{
category: 'Networking',
items: [
{ name: 'SankofaGrid Global Mesh', description: 'Global network mesh that remembers' },
{ name: 'AkanSphere Edge Routing', description: 'Edge routing with cultural intelligence' },
{ name: 'PhoenixFlight Network Fabric', description: 'High-performance network fabric' },
],
},
{
category: 'AI & Machine Learning',
items: [
{ name: 'Firebird AI Engine', description: 'AI that transforms like fire' },
{ name: 'Sankofa Memory Model', description: 'AI models that remember and learn' },
{ name: 'Ancestral Neural Fabric', description: 'Distributed AI with ancestral patterns' },
],
},
{
category: 'Security',
items: [
{ name: 'Aegis of Akan Shield', description: 'Comprehensive security platform' },
{ name: 'PhoenixGuard IAM', description: 'Identity and access management' },
{ name: 'Nsamankom Sentinel', description: 'Security monitoring with ancestral protection' },
],
},
{
category: 'Identity',
items: [
{ name: 'OkraID', description: 'Soul-powered identity framework' },
{ name: 'AkanAuth Sovereign Identity Plane', description: 'Sovereign authentication platform' },
],
},
]
return (
<main className="min-h-screen bg-studio-black">
{/* Header */}
<header className="border-b border-studio-medium bg-studio-dark">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<Link href="/" className="text-2xl font-bold text-white">
Phoenix <span className="text-phoenix-fire">Sankofa</span> Cloud
</Link>
<nav className="flex gap-4">
<Link href="/" className="text-gray-400 hover:text-white">
Home
</Link>
<Link href="/about" className="text-gray-400 hover:text-white">
About
</Link>
<Link href="/manifesto" className="text-gray-400 hover:text-white">
Manifesto
</Link>
</nav>
</div>
</header>
{/* Hero */}
<section className="py-24 px-4">
<div className="mx-auto max-w-4xl text-center">
<h1 className="mb-6 text-5xl font-bold text-white md:text-6xl">
Product Architecture
</h1>
<p className="text-xl text-gray-300">
Infrastructure services themed in Phoenix + Akan cosmology
</p>
</div>
</section>
{/* Products Grid */}
<section className="py-16 px-4">
<div className="mx-auto max-w-7xl space-y-16">
{products.map((category) => (
<div key={category.category}>
<h2 className="mb-8 text-3xl font-bold text-white">
{category.category}
</h2>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{category.items.map((item) => (
<Card key={item.name}>
<CardHeader>
<CardTitle className="text-lg">{item.name}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-gray-400">
{item.description}
</CardDescription>
</CardContent>
</Card>
))}
</div>
</div>
))}
</div>
</section>
{/* CTA */}
<section className="py-16 px-4">
<div className="mx-auto max-w-4xl text-center">
<h2 className="mb-6 text-3xl font-bold text-white">
Ready to Build?
</h2>
<p className="mb-8 text-xl text-gray-400">
Start your sovereign cloud journey today.
</p>
<Link href="/manifesto">
<Button variant="phoenix" size="lg">Learn More</Button>
</Link>
</div>
</section>
</main>
)
}

46
src/app/providers.tsx Normal file
View File

@@ -0,0 +1,46 @@
'use client'
import { ApolloProvider } from '@apollo/client'
import { apolloClient } from '@/lib/graphql/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { Toaster } from '@/components/ui/toaster'
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
retry: (failureCount, error) => {
// Don't retry on 4xx errors
if (error && typeof error === 'object' && 'statusCode' in error) {
const statusCode = error.statusCode as number
if (statusCode >= 400 && statusCode < 500) {
return false
}
}
return failureCount < 3
},
},
mutations: {
retry: false,
},
},
})
)
return (
<ApolloProvider client={apolloClient}>
<QueryClientProvider client={queryClient}>
{children}
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
<Toaster />
</QueryClientProvider>
</ApolloProvider>
)
}

View File

@@ -0,0 +1,135 @@
'use client'
import { useRef, useMemo } from 'react'
import { useFrame } from '@react-three/fiber'
import { Mesh, Vector3 } from 'three'
import * as THREE from 'three'
interface Node {
id: string
name: string
position: [number, number, number]
health: number
type: 'region' | 'cluster' | 'service'
}
interface Edge {
from: string
to: string
latency?: number
bandwidth?: number
}
interface NetworkTopologyProps {
nodes?: Node[]
edges?: Edge[]
}
export default function NetworkTopology({
nodes = defaultNodes,
edges = defaultEdges
}: NetworkTopologyProps) {
const nodeRefs = useRef<{ [key: string]: Mesh }>({})
useFrame((state) => {
// Animate nodes
Object.values(nodeRefs.current).forEach((node, index) => {
if (node) {
node.rotation.y += 0.005
const time = state.clock.elapsedTime
node.position.y = nodes[index]?.position[1] + Math.sin(time + index) * 0.1
}
})
})
const nodePositions = useMemo(() => {
return nodes.map((node) => new Vector3(...node.position))
}, [nodes])
const getNodeColor = (health: number, type: string) => {
if (health >= 90) return '#00FF88'
if (health >= 70) return '#FFB800'
if (health >= 50) return '#FF8C00'
return '#FF0040'
}
const getNodeSize = (type: string) => {
switch (type) {
case 'region': return 0.3
case 'cluster': return 0.2
case 'service': return 0.15
default: return 0.2
}
}
return (
<group>
{/* Render edges/connections */}
{edges.map((edge, index) => {
const fromNode = nodes.find((n) => n.id === edge.from)
const toNode = nodes.find((n) => n.id === edge.to)
if (!fromNode || !toNode) return null
const from = new Vector3(...fromNode.position)
const to = new Vector3(...toNode.position)
return (
<line key={`edge-${index}`}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={2}
array={new Float32Array([
from.x, from.y, from.z,
to.x, to.y, to.z,
])}
itemSize={3}
/>
</bufferGeometry>
<lineBasicMaterial color="#00FFD1" opacity={0.3} transparent />
</line>
)
})}
{/* Render nodes */}
{nodes.map((node, index) => (
<mesh
key={node.id}
ref={(ref) => {
if (ref) nodeRefs.current[node.id] = ref
}}
position={node.position}
>
<sphereGeometry args={[getNodeSize(node.type), 16, 16]} />
<meshStandardMaterial
color={getNodeColor(node.health, node.type)}
emissive={getNodeColor(node.health, node.type)}
emissiveIntensity={0.3}
metalness={0.4}
roughness={0.3}
/>
</mesh>
))}
</group>
)
}
// Default data for demonstration
const defaultNodes: Node[] = [
{ id: '1', name: 'Region 1', position: [-2, 0, 0], health: 95, type: 'region' },
{ id: '2', name: 'Region 2', position: [2, 0, 0], health: 88, type: 'region' },
{ id: '3', name: 'Cluster 1', position: [-1, 1, 0], health: 92, type: 'cluster' },
{ id: '4', name: 'Cluster 2', position: [1, 1, 0], health: 85, type: 'cluster' },
{ id: '5', name: 'Service 1', position: [-1.5, -1, 0], health: 90, type: 'service' },
{ id: '6', name: 'Service 2', position: [1.5, -1, 0], health: 75, type: 'service' },
]
const defaultEdges: Edge[] = [
{ from: '1', to: '2' },
{ from: '1', to: '3' },
{ from: '2', to: '4' },
{ from: '3', to: '5' },
{ from: '4', to: '6' },
]

View File

@@ -0,0 +1,38 @@
'use client'
import { Canvas } from '@react-three/fiber'
import { OrbitControls, PerspectiveCamera, Environment } from '@react-three/drei'
import { Suspense } from 'react'
import NetworkTopology from './NetworkTopology'
export default function NetworkTopologyView() {
return (
<div className="h-[600px] w-full rounded-lg border border-studio-medium bg-studio-black">
<Canvas
gl={{ antialias: true, alpha: false }}
dpr={[1, 2]}
camera={{ position: [0, 0, 8], fov: 50 }}
>
<Suspense fallback={null}>
<PerspectiveCamera makeDefault position={[0, 0, 8]} />
<ambientLight intensity={0.3} />
<pointLight position={[10, 10, 10]} intensity={1} />
<pointLight position={[-10, -10, -10]} intensity={0.5} color="#00FFD1" />
<NetworkTopology />
<OrbitControls
enablePan={true}
enableZoom={true}
enableRotate={true}
minDistance={5}
maxDistance={15}
/>
<Environment preset="night" />
</Suspense>
</Canvas>
</div>
)
}

View File

@@ -0,0 +1,92 @@
'use client'
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import { Mesh } from 'three'
import * as THREE from 'three'
export default function PhoenixSankofaModel() {
const phoenixRef = useRef<Mesh>(null)
const sankofaRef = useRef<Mesh>(null)
// Animate the Phoenix (fire sphere)
useFrame((state) => {
if (phoenixRef.current) {
phoenixRef.current.rotation.y += 0.01
phoenixRef.current.position.y = Math.sin(state.clock.elapsedTime) * 0.2
}
if (sankofaRef.current) {
sankofaRef.current.rotation.y -= 0.01
sankofaRef.current.position.y = Math.sin(state.clock.elapsedTime + Math.PI) * 0.2
}
})
return (
<group>
{/* Phoenix Fire - Red/Orange sphere */}
<mesh ref={phoenixRef} position={[-1, 0, 0]}>
<sphereGeometry args={[0.8, 32, 32]} />
<meshStandardMaterial
color="#FF4500"
emissive="#FF4500"
emissiveIntensity={0.5}
metalness={0.3}
roughness={0.2}
/>
</mesh>
{/* Sankofa Gold - Gold sphere */}
<mesh ref={sankofaRef} position={[1, 0, 0]}>
<sphereGeometry args={[0.8, 32, 32]} />
<meshStandardMaterial
color="#FFD700"
emissive="#FFD700"
emissiveIntensity={0.3}
metalness={0.5}
roughness={0.2}
/>
</mesh>
{/* Connection line between Phoenix and Sankofa */}
<line>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={2}
array={new Float32Array([-1, 0, 0, 1, 0, 0])}
itemSize={3}
/>
</bufferGeometry>
<lineBasicMaterial color="#00FFD1" linewidth={2} />
</line>
{/* Central node representing the integration */}
<mesh position={[0, 0, 0]}>
<torusGeometry args={[0.3, 0.1, 16, 32]} />
<meshStandardMaterial
color="#6A0DAD"
emissive="#6A0DAD"
emissiveIntensity={0.4}
metalness={0.6}
roughness={0.1}
/>
</mesh>
{/* Particle effects around the model */}
<points>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={100}
array={new Float32Array(
Array.from({ length: 300 }, () => (Math.random() - 0.5) * 4)
)}
itemSize={3}
/>
</bufferGeometry>
<pointsMaterial size={0.05} color="#00FFFF" />
</points>
</group>
)
}

View File

@@ -0,0 +1,38 @@
'use client'
import { Canvas } from '@react-three/fiber'
import { OrbitControls, Environment, PerspectiveCamera } from '@react-three/drei'
import { Suspense } from 'react'
import PhoenixSankofaModel from './PhoenixSankofaModel'
export default function PhoenixSankofaScene() {
return (
<div className="h-screen w-full bg-studio-black">
<Canvas
gl={{ antialias: true, alpha: false }}
dpr={[1, 2]}
camera={{ position: [0, 0, 5], fov: 50 }}
>
<Suspense fallback={null}>
<PerspectiveCamera makeDefault position={[0, 0, 5]} />
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} intensity={1} />
<pointLight position={[-10, -10, -10]} intensity={0.5} color="#FF4500" />
<PhoenixSankofaModel />
<OrbitControls
enablePan={true}
enableZoom={true}
enableRotate={true}
minDistance={3}
maxDistance={10}
/>
<Environment preset="night" />
</Suspense>
</Canvas>
</div>
)
}

View File

@@ -0,0 +1,55 @@
'use client'
import MetricsCard from './MetricsCard'
export default function Dashboard() {
return (
<div className="space-y-6 p-6">
<h1 className="text-3xl font-bold text-white">Dashboard</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<MetricsCard
title="Total Regions"
value="325"
health={95}
trend="up"
description="Active global regions"
/>
<MetricsCard
title="Active Services"
value="1,247"
health={88}
trend="stable"
description="Running services"
/>
<MetricsCard
title="Network Health"
value="92%"
health={92}
trend="up"
description="Overall network status"
/>
<MetricsCard
title="Cost Efficiency"
value="87%"
health={87}
trend="up"
description="Cost optimization score"
/>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="rounded-lg border border-studio-medium bg-studio-dark p-6">
<h2 className="mb-4 text-xl font-semibold text-white">Performance Metrics</h2>
<p className="text-gray-400">Chart placeholder - ECharts integration</p>
</div>
<div className="rounded-lg border border-studio-medium bg-studio-dark p-6">
<h2 className="mb-4 text-xl font-semibold text-white">Cost Analysis</h2>
<p className="text-gray-400">Chart placeholder - ECharts integration</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,52 @@
'use client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { getHealthScoreColor } from '@/lib/design-system'
interface MetricsCardProps {
title: string
value: string | number
health?: number
trend?: 'up' | 'down' | 'stable'
description?: string
}
export default function MetricsCard({
title,
value,
health,
trend,
description
}: MetricsCardProps) {
const healthColor = health !== undefined ? getHealthScoreColor(health) : undefined
return (
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-400">{title}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold text-white">{value}</span>
{health !== undefined && (
<span
className="text-sm font-semibold"
style={{ color: healthColor }}
>
{health}%
</span>
)}
</div>
{description && (
<p className="mt-2 text-sm text-gray-400">{description}</p>
)}
{trend && (
<div className="mt-2 text-xs text-gray-500">
Trend: {trend}
</div>
)}
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,44 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
src?: string
alt?: string
fallback?: string
size?: 'sm' | 'md' | 'lg'
}
const sizeClasses = {
sm: 'h-8 w-8 text-xs',
md: 'h-10 w-10 text-sm',
lg: 'h-12 w-12 text-base',
}
export function Avatar({ src, alt, fallback, size = 'md', className, ...props }: AvatarProps) {
const [error, setError] = React.useState(false)
return (
<div
className={cn(
'relative flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-studio-medium',
sizeClasses[size],
className
)}
{...props}
>
{src && !error ? (
<img
src={src}
alt={alt}
className="h-full w-full object-cover"
onError={() => setError(true)}
/>
) : (
<span className="font-medium text-foreground">
{fallback || (alt ? alt.charAt(0).toUpperCase() : '?')}
</span>
)}
</div>
)
}

View File

@@ -0,0 +1,33 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-phoenix-fire focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-studio-medium bg-studio-medium text-foreground',
success: 'border-status-success bg-status-success/10 text-status-success',
error: 'border-status-error bg-status-error/10 text-status-error',
warning: 'border-status-warning bg-status-warning/10 text-status-warning',
info: 'border-status-info bg-status-info/10 text-status-info',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,28 @@
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<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border border-studio-medium bg-studio-dark px-3 py-1.5 text-sm text-foreground shadow-md 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',
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -0,0 +1,77 @@
'use client'
import { useCallback } from 'react'
import ReactFlow, {
Node,
Edge,
addEdge,
Connection,
useNodesState,
useEdgesState,
Controls,
MiniMap,
Background,
} from 'reactflow'
import 'reactflow/dist/style.css'
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Region 1' },
position: { x: 250, y: 5 },
style: { background: '#1E3A8A', color: '#fff' },
},
{
id: '2',
data: { label: 'Cluster 1' },
position: { x: 100, y: 100 },
style: { background: '#FF4500', color: '#fff' },
},
{
id: '3',
data: { label: 'Service 1' },
position: { x: 400, y: 100 },
style: { background: '#FFD700', color: '#000' },
},
]
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
]
export default function TopologyEditor() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
const onConnect = useCallback(
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
)
return (
<div className="h-[600px] w-full rounded-lg border border-studio-medium bg-studio-dark">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
className="bg-studio-black"
>
<Controls className="bg-studio-dark border-studio-medium" />
<MiniMap
className="bg-studio-dark border-studio-medium"
nodeColor={(node) => {
if (node.type === 'input') return '#1E3A8A'
return '#FF4500'
}}
/>
<Background color="#2A2A2A" gap={16} />
</ReactFlow>
</div>
)
}

View File

@@ -0,0 +1,103 @@
'use client'
import React, { Component, ErrorInfo, ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error: Error | null
errorInfo: ErrorInfo | null
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
hasError: false,
error: null,
errorInfo: null,
}
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log error to monitoring service
if (typeof window !== 'undefined') {
const { logError } = require('@/lib/monitoring')
logError(error, { errorInfo })
}
this.setState({
error,
errorInfo,
})
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
})
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback
}
return <ErrorFallback error={this.state.error} onReset={this.handleReset} />
}
return this.props.children
}
}
interface ErrorFallbackProps {
error: Error | null
onReset: () => void
}
function ErrorFallback({ error, onReset }: ErrorFallbackProps) {
return (
<div className="flex min-h-screen items-center justify-center bg-studio-black p-4">
<Card className="max-w-2xl">
<CardHeader>
<CardTitle className="text-status-error">Something went wrong</CardTitle>
<CardDescription>
An unexpected error occurred. Please try again or contact support if the problem persists.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<div className="rounded-md bg-studio-medium p-4">
<p className="font-mono text-sm text-status-error">{error.message}</p>
{process.env.NODE_ENV === 'development' && error.stack && (
<pre className="mt-2 overflow-auto text-xs text-gray-400">{error.stack}</pre>
)}
</div>
)}
<div className="flex gap-2">
<Button onClick={onReset} variant="phoenix">
Try Again
</Button>
<Button onClick={() => window.location.reload()} variant="outline">
Reload Page
</Button>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,41 @@
'use client'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
interface ErrorFallbackProps {
error: Error
resetErrorBoundary: () => void
}
export function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) {
return (
<div className="flex min-h-screen items-center justify-center bg-studio-black p-4">
<Card className="max-w-2xl">
<CardHeader>
<CardTitle className="text-status-error">Something went wrong</CardTitle>
<CardDescription>
An unexpected error occurred. Please try again or contact support if the problem persists.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-md bg-studio-medium p-4">
<p className="font-mono text-sm text-status-error">{error.message}</p>
{process.env.NODE_ENV === 'development' && error.stack && (
<pre className="mt-2 overflow-auto text-xs text-gray-400">{error.stack}</pre>
)}
</div>
<div className="flex gap-2">
<Button onClick={resetErrorBoundary} variant="phoenix">
Try Again
</Button>
<Button onClick={() => window.location.reload()} variant="outline">
Reload Page
</Button>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,62 @@
import Link from 'next/link'
export function Footer() {
return (
<footer className="border-t border-studio-medium bg-studio-black">
<div className="container py-12">
<div className="grid grid-cols-1 gap-8 md:grid-cols-4">
<div>
<h3 className="mb-4 text-lg font-semibold">Phoenix Sankofa Cloud</h3>
<p className="text-sm text-gray-400">
The sovereign cloud born of fire and ancestral wisdom.
</p>
</div>
<div>
<h4 className="mb-4 text-sm font-semibold">Product</h4>
<ul className="space-y-2 text-sm text-gray-400">
<li>
<Link href="/products" className="hover:text-phoenix-fire">
Products
</Link>
</li>
<li>
<Link href="/about" className="hover:text-phoenix-fire">
About
</Link>
</li>
</ul>
</div>
<div>
<h4 className="mb-4 text-sm font-semibold">Resources</h4>
<ul className="space-y-2 text-sm text-gray-400">
<li>
<Link href="/manifesto" className="hover:text-phoenix-fire">
Manifesto
</Link>
</li>
<li>
<Link href="/docs" className="hover:text-phoenix-fire">
Documentation
</Link>
</li>
</ul>
</div>
<div>
<h4 className="mb-4 text-sm font-semibold">Company</h4>
<ul className="space-y-2 text-sm text-gray-400">
<li>
<Link href="/about" className="hover:text-phoenix-fire">
About Us
</Link>
</li>
</ul>
</div>
</div>
<div className="mt-8 border-t border-studio-medium pt-8 text-center text-sm text-gray-400">
<p>&copy; {new Date().getFullYear()} Phoenix Sankofa Cloud. All rights reserved.</p>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,36 @@
import Link from 'next/link'
import { Button } from '@/components/ui/button'
export function Header() {
return (
<header className="sticky top-0 z-50 w-full border-b border-studio-medium bg-studio-black/95 backdrop-blur supports-[backdrop-filter]:bg-studio-black/60">
<div className="container flex h-16 items-center">
<Link href="/" className="mr-6 flex items-center space-x-2">
<span className="text-xl font-bold bg-gradient-to-r from-phoenix-fire to-sankofa-gold bg-clip-text text-transparent">
Phoenix Sankofa Cloud
</span>
</Link>
<nav className="flex flex-1 items-center space-x-6 text-sm font-medium">
<Link href="/products" className="transition-colors hover:text-phoenix-fire">
Products
</Link>
<Link href="/about" className="transition-colors hover:text-phoenix-fire">
About
</Link>
<Link href="/manifesto" className="transition-colors hover:text-phoenix-fire">
Manifesto
</Link>
</nav>
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm">
Sign In
</Button>
<Button variant="phoenix" size="sm">
Get Started
</Button>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,42 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
interface NavItem {
title: string
href: string
}
const navItems: NavItem[] = [
{ title: 'Home', href: '/' },
{ title: 'Products', href: '/products' },
{ title: 'About', href: '/about' },
{ title: 'Manifesto', href: '/manifesto' },
]
export function Navbar() {
const pathname = usePathname()
return (
<nav className="flex space-x-6">
{navItems.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
className={cn(
'text-sm font-medium transition-colors hover:text-phoenix-fire',
isActive ? 'text-phoenix-fire' : 'text-gray-400'
)}
>
{item.title}
</Link>
)
})}
</nav>
)
}

View File

@@ -0,0 +1,49 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
import { LucideIcon } from 'lucide-react'
interface SidebarItem {
title: string
href: string
icon?: LucideIcon
}
interface SidebarProps {
items: SidebarItem[]
className?: string
}
export function Sidebar({ items, className }: SidebarProps) {
const pathname = usePathname()
return (
<aside className={cn('w-64 border-r border-studio-medium bg-studio-black p-4', className)}>
<nav className="space-y-1">
{items.map((item) => {
const isActive = pathname === item.href
const Icon = item.icon
return (
<Link
key={item.href}
href={item.href}
className={cn(
'flex items-center space-x-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-studio-medium text-phoenix-fire'
: 'text-gray-400 hover:bg-studio-medium hover:text-foreground'
)}
>
{Icon && <Icon className="h-4 w-4" />}
<span>{item.title}</span>
</Link>
)
})}
</nav>
</aside>
)
}

View File

@@ -0,0 +1,58 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@/lib/test-utils'
import userEvent from '@testing-library/user-event'
import { Button } from './button'
describe('Button Component', () => {
it('should render button with text', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument()
})
it('should handle click events', async () => {
const handleClick = vi.fn()
const user = userEvent.setup()
render(<Button onClick={handleClick}>Click me</Button>)
await user.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should apply variant classes', () => {
const { rerender } = render(<Button variant="phoenix">Phoenix</Button>)
expect(screen.getByRole('button')).toHaveClass('bg-phoenix-fire')
rerender(<Button variant="sankofa">Sankofa</Button>)
expect(screen.getByRole('button')).toHaveClass('bg-sankofa-gold')
rerender(<Button variant="outline">Outline</Button>)
expect(screen.getByRole('button')).toHaveClass('border')
})
it('should apply size classes', () => {
const { rerender } = render(<Button size="sm">Small</Button>)
expect(screen.getByRole('button')).toHaveClass('h-8')
rerender(<Button size="lg">Large</Button>)
expect(screen.getByRole('button')).toHaveClass('h-12')
})
it('should be disabled when disabled prop is true', () => {
render(<Button disabled>Disabled</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
it('should not call onClick when disabled', async () => {
const handleClick = vi.fn()
const user = userEvent.setup()
render(
<Button disabled onClick={handleClick}>
Disabled
</Button>
)
await user.click(screen.getByRole('button'))
expect(handleClick).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,40 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'phoenix' | 'sankofa' | 'outline' | 'ghost'
size?: 'sm' | 'md' | 'lg'
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'default', size = 'md', ...props }, ref) => {
const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'
const variants = {
default: 'bg-studio-medium text-foreground hover:bg-studio-dark',
phoenix: 'bg-phoenix-fire text-white hover:bg-phoenix-flame glow-phoenix',
sankofa: 'bg-sankofa-gold text-studio-black hover:bg-yellow-400 glow-sankofa',
outline: 'border border-studio-medium bg-transparent hover:bg-studio-medium',
ghost: 'hover:bg-studio-medium',
}
const sizes = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-base',
lg: 'h-12 px-6 text-lg',
}
return (
<button
className={cn(baseStyles, variants[variant], sizes[size], className)}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = 'Button'
export { Button }

View File

@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@/lib/test-utils'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './card'
describe('Card Components', () => {
it('should render Card with children', () => {
render(
<Card>
<p>Card content</p>
</Card>
)
expect(screen.getByText('Card content')).toBeInTheDocument()
})
it('should render CardHeader with title and description', () => {
render(
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card Description</CardDescription>
</CardHeader>
</Card>
)
expect(screen.getByText('Card Title')).toBeInTheDocument()
expect(screen.getByText('Card Description')).toBeInTheDocument()
})
it('should render CardContent', () => {
render(
<Card>
<CardContent>
<p>Content here</p>
</CardContent>
</Card>
)
expect(screen.getByText('Content here')).toBeInTheDocument()
})
it('should render CardFooter', () => {
render(
<Card>
<CardFooter>
<button>Action</button>
</CardFooter>
</Card>
)
expect(screen.getByRole('button', { name: /action/i })).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,76 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-lg border border-studio-medium bg-studio-dark p-6 shadow-lg',
className
)}
{...props}
/>
))
Card.displayName = 'Card'
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5', className)}
{...props}
/>
))
CardHeader.displayName = 'CardHeader'
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
{...props}
/>
))
CardTitle.displayName = 'CardTitle'
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-gray-400', className)}
{...props}
/>
))
CardDescription.displayName = 'CardDescription'
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('pt-6', className)} {...props} />
))
CardContent.displayName = 'CardContent'
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center pt-6', className)}
{...props}
/>
))
CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,102 @@
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<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ 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<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ 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 border-studio-medium bg-studio-dark 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-studio-black transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-phoenix-fire focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-studio-medium data-[state=open]:text-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 }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
)
DialogHeader.displayName = 'DialogHeader'
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<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<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ 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<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-gray-400', className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,183 @@
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<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.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-studio-medium data-[state=open]:bg-studio-medium',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-studio-medium bg-studio-dark p-1 text-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',
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-studio-medium bg-studio-dark p-1 text-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',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-studio-medium focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ 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-studio-medium focus:text-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<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ 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-studio-medium focus:text-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<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ 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<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-studio-medium', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
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,
}

157
src/components/ui/form.tsx Normal file
View File

@@ -0,0 +1,157 @@
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from 'react-hook-form'
import { cn } from '@/lib/utils'
import { Label } from './label'
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
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()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>')
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ 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<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && 'text-status-error', className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = 'FormLabel'
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...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<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn('text-sm text-gray-400', className)}
{...props}
/>
)
})
FormDescription.displayName = 'FormDescription'
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ 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-sm font-medium text-status-error', className)}
{...props}
>
{body}
</p>
)
}
)
FormMessage.displayName = 'FormMessage'
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField }

View File

@@ -0,0 +1,25 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-studio-medium bg-studio-dark px-3 py-2 text-sm text-foreground ring-offset-studio-black file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-phoenix-fire focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
aria-label={props['aria-label'] || (props.placeholder ? `Input: ${props.placeholder}` : 'Input field')}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }

View File

@@ -0,0 +1,20 @@
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } 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<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,29 @@
import { cn } from '@/lib/utils'
interface LoadingProps {
className?: string
size?: 'sm' | 'md' | 'lg'
}
export function Loading({ className, size = 'md' }: LoadingProps) {
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-8 w-8',
lg: 'h-12 w-12',
}
return (
<div
className={cn(
'animate-spin rounded-full border-2 border-studio-medium border-t-phoenix-fire',
sizeClasses[size],
className
)}
role="status"
aria-label="Loading"
>
<span className="sr-only">Loading...</span>
</div>
)
}

View File

@@ -0,0 +1,151 @@
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<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-studio-medium bg-studio-dark px-3 py-2 text-sm text-foreground ring-offset-studio-black placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-phoenix-fire focus:ring-offset-2 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<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ 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<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ 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<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-studio-medium bg-studio-dark text-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',
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<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ 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-8 pr-2 text-sm outline-none focus:bg-studio-medium focus:text-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">
<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<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-studio-medium', className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,15 @@
import { cn } from '@/lib/utils'
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string
}
export function Skeleton({ className, ...props }: SkeletonProps) {
return (
<div
className={cn('animate-pulse rounded-md bg-studio-medium', className)}
{...props}
/>
)
}

View File

@@ -0,0 +1,87 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ 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<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
))
TableHeader.displayName = 'TableHeader'
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
))
TableBody.displayName = 'TableBody'
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn('border-t border-studio-medium bg-studio-medium font-medium [&>tr]:last:border-b-0', className)}
{...props}
/>
))
TableFooter.displayName = 'TableFooter'
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b border-studio-medium transition-colors hover:bg-studio-medium/50 data-[state=selected]:bg-studio-medium',
className
)}
{...props}
/>
)
)
TableRow.displayName = 'TableRow'
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-12 px-4 text-left align-middle font-medium text-foreground [&:has([role=checkbox])]:pr-0',
className
)}
{...props}
/>
))
TableHead.displayName = 'TableHead'
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td ref={ref} className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)} {...props} />
))
TableCell.displayName = 'TableCell'
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('mt-4 text-sm text-gray-400', className)} {...props} />
))
TableCaption.displayName = 'TableCaption'
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }

View File

@@ -0,0 +1,53 @@
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<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-studio-medium p-1 text-gray-400',
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-studio-black transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-phoenix-fire focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-studio-dark data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-studio-black focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-phoenix-fire focus-visible:ring-offset-2',
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

121
src/components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,121 @@
import * as React from 'react'
import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ 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-4 overflow-hidden rounded-md border border-studio-medium bg-studio-dark p-6 pr-8 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-studio-medium bg-studio-dark text-foreground',
success: 'border-status-success bg-status-success/10 text-status-success',
error: 'border-status-error bg-status-error/10 text-status-error',
warning: 'border-status-warning bg-status-warning/10 text-status-warning',
info: 'border-status-info bg-status-info/10 text-status-info',
},
},
defaultVariants: {
variant: 'default',
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-studio-medium bg-studio-dark px-3 text-sm font-medium ring-offset-studio-black transition-colors hover:bg-studio-medium focus:outline-none focus:ring-2 focus:ring-phoenix-fire focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-status-error group-[.destructive]:hover:border-status-error group-[.destructive]:hover:bg-status-error group-[.destructive]:hover:text-foreground group-[.destructive]:focus:ring-status-error',
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-status-error group-[.destructive]:hover:text-status-error group-[.destructive]:focus:ring-status-error',
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title ref={ref} className={cn('text-sm font-semibold', className)} {...props} />
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description ref={ref} className={cn('text-sm opacity-90', className)} {...props} />
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@@ -0,0 +1,34 @@
'use client'
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from './toast'
import { useToast } from './use-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>
)
}

View File

@@ -0,0 +1,186 @@
import * as React from 'react'
import type { ToastActionElement, ToastProps } from './toast'
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST',
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType['ADD_TOAST']
toast: ToasterToast
}
| {
type: ActionType['UPDATE_TOAST']
toast: Partial<ToasterToast>
}
| {
type: ActionType['DISMISS_TOAST']
toastId?: ToasterToast['id']
}
| {
type: ActionType['REMOVE_TOAST']
toastId?: ToasterToast['id']
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
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: State, action: Action): State => {
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
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: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, 'id'>
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
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<State>(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?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
}
}
export { useToast, toast }

View File

@@ -0,0 +1,41 @@
'use client'
import { Button } from '@/components/ui/button'
import { pillars, getPillarColor } from '@/lib/design-system'
type PillarCode = keyof typeof pillars
interface LensSelectorProps {
selectedLens: PillarCode | null
onLensChange: (lens: PillarCode | null) => void
}
export default function LensSelector({ selectedLens, onLensChange }: LensSelectorProps) {
return (
<div className="flex flex-wrap gap-2">
<Button
variant={selectedLens === null ? 'phoenix' : 'outline'}
size="sm"
onClick={() => onLensChange(null)}
>
All Pillars
</Button>
{Object.entries(pillars).map(([code, pillar]) => (
<Button
key={code}
variant={selectedLens === code ? 'phoenix' : 'outline'}
size="sm"
onClick={() => onLensChange(code as PillarCode)}
style={
selectedLens === code
? { backgroundColor: getPillarColor(code as PillarCode) }
: {}
}
>
{pillar.name}
</Button>
))}
</div>
)
}

View File

@@ -0,0 +1,125 @@
'use client'
import { useState } from 'react'
import LensSelector from './LensSelector'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { getHealthScoreColor, getPillarColor, pillars } from '@/lib/design-system'
import type { PillarCode } from './LensSelector'
interface Finding {
id: string
control: string
resource: string
status: 'PASS' | 'FAIL' | 'WARNING'
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
title: string
}
const mockFindings: Finding[] = [
{
id: '1',
control: 'SECURITY-001',
resource: 'Region 1',
status: 'PASS',
severity: 'LOW',
title: 'Encryption at rest enabled',
},
{
id: '2',
control: 'RELIABILITY-002',
resource: 'Cluster 1',
status: 'WARNING',
severity: 'MEDIUM',
title: 'Backup frequency below recommended',
},
{
id: '3',
control: 'COST-003',
resource: 'Service 1',
status: 'FAIL',
severity: 'HIGH',
title: 'Resource over-provisioned',
},
]
export default function WAFDashboard() {
const [selectedLens, setSelectedLens] = useState<PillarCode | null>(null)
const filteredFindings = selectedLens
? mockFindings.filter((f) => f.control.startsWith(selectedLens))
: mockFindings
return (
<div className="space-y-6 p-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold text-white">Well-Architected Framework</h1>
<LensSelector selectedLens={selectedLens} onLensChange={setSelectedLens} />
</div>
<div className="grid gap-6 md:grid-cols-3 lg:grid-cols-6">
{Object.entries(pillars).map(([code, pillar]) => {
const score = Math.floor(Math.random() * 30) + 70 // Mock score 70-100
const color = getHealthScoreColor(score)
return (
<Card key={code}>
<CardHeader>
<CardTitle className="text-sm" style={{ color: getPillarColor(code as PillarCode) }}>
{pillar.name}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold" style={{ color }}>
{score}%
</div>
</CardContent>
</Card>
)
})}
</div>
<Card>
<CardHeader>
<CardTitle>Findings</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{filteredFindings.map((finding) => (
<div
key={finding.id}
className="rounded-lg border border-studio-medium bg-studio-dark p-4"
>
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-white">{finding.title}</h3>
<p className="text-sm text-gray-400">
{finding.control} {finding.resource}
</p>
</div>
<div className="flex items-center gap-2">
<span
className="rounded px-2 py-1 text-xs font-semibold"
style={{
backgroundColor:
finding.status === 'PASS'
? '#00FF88'
: finding.status === 'WARNING'
? '#FFB800'
: '#FF0040',
color: '#000',
}}
>
{finding.status}
</span>
<span className="text-xs text-gray-400">{finding.severity}</span>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}

17
src/content/brand.md Normal file
View File

@@ -0,0 +1,17 @@
# Phoenix Sankofa Cloud Brand Content
## Primary Tagline
The sovereign cloud born of fire and ancestral wisdom.
## Mission Statement
Phoenix Sankofa Cloud exists to build the world's first sovereign AI cloud infrastructure that honors ancestral wisdom, reflects cultural identity, serves global sovereignty, and transforms through the power of rebirth and return.
## Core Values
- Remember: Where we came from
- Retrieve: What was essential
- Restore: Identity and sovereignty
- Rise: Forward with purpose
## Brand Promise
We promise infrastructure that remembers its origins, retrieves ancestral wisdom, restores identity and sovereignty, and rises forward with purpose.

93
src/lib/accessibility.ts Normal file
View File

@@ -0,0 +1,93 @@
/**
* Accessibility utilities
*/
/**
* Get accessible label for an element
*/
export function getAccessibleLabel(
label?: string,
placeholder?: string,
fallback = 'Interactive element'
): string {
return label || placeholder || fallback
}
/**
* Generate unique ID for form elements
*/
export function generateId(prefix = 'id'): string {
return `${prefix}-${Math.random().toString(36).substring(2, 9)}`
}
/**
* Check if element is focusable
*/
export function isFocusable(element: HTMLElement): boolean {
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
].join(', ')
return element.matches(focusableSelectors)
}
/**
* Trap focus within a container
*/
export function trapFocus(container: HTMLElement): () => void {
const focusableElements = container.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
const handleTab = (e: KeyboardEvent) => {
if (e.key !== 'Tab') {
return
}
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault()
lastElement?.focus()
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault()
firstElement?.focus()
}
}
}
container.addEventListener('keydown', handleTab)
// Return cleanup function
return () => {
container.removeEventListener('keydown', handleTab)
}
}
/**
* Announce message to screen readers
*/
export function announceToScreenReader(message: string, priority: 'polite' | 'assertive' = 'polite') {
const announcement = document.createElement('div')
announcement.setAttribute('role', 'status')
announcement.setAttribute('aria-live', priority)
announcement.setAttribute('aria-atomic', 'true')
announcement.className = 'sr-only'
announcement.textContent = message
document.body.appendChild(announcement)
setTimeout(() => {
document.body.removeChild(announcement)
}, 1000)
}

53
src/lib/auth-storage.ts Normal file
View File

@@ -0,0 +1,53 @@
/**
* Secure authentication token storage
*
* This module provides an abstraction for storing authentication tokens
* securely. In production, tokens should be stored in httpOnly cookies,
* but for development we can use a more permissive approach.
*/
/**
* Store authentication token
*/
export function setAuthToken(token: string): void {
if (typeof window === 'undefined') {
return
}
// In production, this should set an httpOnly cookie via API
// For now, we'll use sessionStorage as a compromise (better than localStorage)
// TODO: Implement httpOnly cookie storage via API endpoint
sessionStorage.setItem('auth_token', token)
}
/**
* Get authentication token
*/
export function getAuthToken(): string | null {
if (typeof window === 'undefined') {
return null
}
// TODO: Get from httpOnly cookie via API endpoint
return sessionStorage.getItem('auth_token')
}
/**
* Remove authentication token
*/
export function removeAuthToken(): void {
if (typeof window === 'undefined') {
return
}
sessionStorage.removeItem('auth_token')
// TODO: Clear httpOnly cookie via API endpoint
}
/**
* Check if user is authenticated
*/
export function isAuthenticated(): boolean {
return getAuthToken() !== null
}

111
src/lib/content.ts Normal file
View File

@@ -0,0 +1,111 @@
/**
* Content Management Utilities
*
* Functions for loading and managing brand content
*/
export interface BrandContent {
tagline: string
mission: string
values: string[]
promise: string
}
/**
* Load brand content
* In production, this could load from CMS, API, or database
*/
export async function loadBrandContent(): Promise<BrandContent> {
// For now, return static content
// In production, this would fetch from API, CMS, or database
return {
tagline: 'The sovereign cloud born of fire and ancestral wisdom.',
mission: 'Phoenix Sankofa Cloud exists to build the world\'s first sovereign AI cloud infrastructure that honors ancestral wisdom, reflects cultural identity, serves global sovereignty, and transforms through the power of rebirth and return.',
values: [
'Remember: Where we came from',
'Retrieve: What was essential',
'Restore: Identity and sovereignty',
'Rise: Forward with purpose',
],
promise: 'We promise infrastructure that remembers its origins, retrieves ancestral wisdom, restores identity and sovereignty, and rises forward with purpose.',
}
}
/**
* Get product name by category
*/
export function getProductName(category: string, index: number): string {
const productNames: Record<string, string[]> = {
compute: [
'PhoenixCore Compute',
'SankofaEdge Nodes',
'AkanFire VM Engine',
],
storage: [
'OkraVault Storage',
'Nananom Archive',
'Egg of the Phoenix Object Store',
],
networking: [
'SankofaGrid Global Mesh',
'AkanSphere Edge Routing',
'PhoenixFlight Network Fabric',
],
ai: [
'Firebird AI Engine',
'Sankofa Memory Model',
'Ancestral Neural Fabric',
],
security: [
'Aegis of Akan Shield',
'PhoenixGuard IAM',
'Nsamankom Sentinel',
],
identity: [
'OkraID',
'AkanAuth Sovereign Identity Plane',
],
}
return productNames[category.toLowerCase()]?.[index] || 'Unknown Product'
}
/**
* Get product description
*/
export function getProductDescription(category: string, index: number): string {
const descriptions: Record<string, string[]> = {
compute: [
'Core compute engine powered by Phoenix fire',
'Edge nodes that remember and return data',
'High-performance VMs with Akan heritage',
],
storage: [
'Storage for the soul of your data',
'Long-term archival with ancestral memory',
'Object storage for transformation',
],
networking: [
'Global network mesh that remembers',
'Edge routing with cultural intelligence',
'High-performance network fabric',
],
ai: [
'AI that transforms like fire',
'AI models that remember and learn',
'Distributed AI with ancestral patterns',
],
security: [
'Comprehensive security platform',
'Identity and access management',
'Security monitoring with ancestral protection',
],
identity: [
'Soul-powered identity framework',
'Sovereign authentication platform',
],
}
return descriptions[category.toLowerCase()]?.[index] || 'Product description'
}

View File

@@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest'
import { getHealthScoreColor, getPillarColor, pillars } from './design-system'
describe('Design System Utilities', () => {
describe('getHealthScoreColor', () => {
it('should return excellent color for scores >= 90', () => {
expect(getHealthScoreColor(90)).toBe('#00FF88')
expect(getHealthScoreColor(100)).toBe('#00FF88')
})
it('should return good color for scores >= 70', () => {
expect(getHealthScoreColor(70)).toBe('#FFB800')
expect(getHealthScoreColor(89)).toBe('#FFB800')
})
it('should return fair color for scores >= 50', () => {
expect(getHealthScoreColor(50)).toBe('#FF8C00')
expect(getHealthScoreColor(69)).toBe('#FF8C00')
})
it('should return poor color for scores < 50', () => {
expect(getHealthScoreColor(49)).toBe('#FF0040')
expect(getHealthScoreColor(0)).toBe('#FF0040')
})
})
describe('getPillarColor', () => {
it('should return correct color for each pillar', () => {
expect(getPillarColor('SECURITY')).toBe('#FF0040')
expect(getPillarColor('RELIABILITY')).toBe('#00FF88')
expect(getPillarColor('COST_OPTIMIZATION')).toBe('#00FFFF')
expect(getPillarColor('PERFORMANCE_EFFICIENCY')).toBe('#00FFD1')
expect(getPillarColor('OPERATIONAL_EXCELLENCE')).toBe('#FF00FF')
expect(getPillarColor('SUSTAINABILITY')).toBe('#00FF88')
})
it('should return default color for invalid pillar', () => {
// @ts-expect-error - testing invalid input
expect(getPillarColor('INVALID')).toBe('#00FFFF')
})
})
})

150
src/lib/design-system.ts Normal file
View File

@@ -0,0 +1,150 @@
/**
* Phoenix Sankofa Cloud Design System
*
* Design tokens and utilities for the brand
*/
// Color Palette
export const colors = {
phoenix: {
fire: '#FF4500',
flame: '#FF8C00',
ember: '#FF6B35',
},
sankofa: {
gold: '#FFD700',
earth: '#8B4513',
},
sovereignty: {
purple: '#6A0DAD',
deep: '#4B0082',
},
ancestral: {
blue: '#1E3A8A',
deep: '#0F1B3D',
},
studio: {
black: '#0A0A0A',
dark: '#1A1A1A',
medium: '#2A2A2A',
},
neon: {
teal: '#00FFD1',
magenta: '#FF00FF',
cyan: '#00FFFF',
amber: '#FFB800',
},
status: {
success: '#00FF88',
warning: '#FFB800',
error: '#FF0040',
info: '#00B8FF',
},
} as const
// Typography Scale
export const typography = {
fontFamily: {
sans: 'var(--font-inter), system-ui, sans-serif',
mono: 'var(--font-mono), monospace',
},
fontSize: {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
base: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '2rem', // 32px
'4xl': '3rem', // 48px
'5xl': '4rem', // 64px
},
fontWeight: {
light: 300,
regular: 400,
medium: 500,
semibold: 600,
bold: 700,
},
} as const
// Spacing Scale (4px base unit)
export const spacing = {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
'2xl': '48px',
'3xl': '64px',
'4xl': '96px',
'5xl': '128px',
} as const
// Animation Durations
export const animation = {
fast: '150ms',
normal: '200ms',
slow: '300ms',
slower: '500ms',
} as const
// Well-Architected Framework Pillars
export const pillars = {
SECURITY: {
code: 'SECURITY',
name: 'Security',
color: colors.status.error,
},
RELIABILITY: {
code: 'RELIABILITY',
name: 'Reliability',
color: colors.status.success,
},
COST_OPTIMIZATION: {
code: 'COST_OPTIMIZATION',
name: 'Cost Optimization',
color: colors.neon.cyan,
},
PERFORMANCE_EFFICIENCY: {
code: 'PERFORMANCE_EFFICIENCY',
name: 'Performance Efficiency',
color: colors.neon.teal,
},
OPERATIONAL_EXCELLENCE: {
code: 'OPERATIONAL_EXCELLENCE',
name: 'Operational Excellence',
color: colors.neon.magenta,
},
SUSTAINABILITY: {
code: 'SUSTAINABILITY',
name: 'Sustainability',
color: colors.status.success,
},
} as const
// Health Score Colors
export const healthScoreColors = {
excellent: colors.status.success, // 90-100
good: colors.neon.amber, // 70-89
fair: colors.phoenix.flame, // 50-69
poor: colors.status.error, // 0-49
} as const
/**
* Get health score color based on score (0-100)
*/
export function getHealthScoreColor(score: number): string {
if (score >= 90) return healthScoreColors.excellent
if (score >= 70) return healthScoreColors.good
if (score >= 50) return healthScoreColors.fair
return healthScoreColors.poor
}
/**
* Get pillar color
*/
export function getPillarColor(pillarCode: keyof typeof pillars): string {
return pillars[pillarCode]?.color || colors.neon.cyan
}

59
src/lib/env.ts Normal file
View File

@@ -0,0 +1,59 @@
/**
* Environment variable validation and access
*/
const requiredEnvVars = {
// Add required env vars here as they are needed
// NEXT_PUBLIC_GRAPHQL_ENDPOINT: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT,
} as const
const optionalEnvVars = {
NEXT_PUBLIC_GRAPHQL_ENDPOINT: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || '/api/graphql',
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
NODE_ENV: process.env.NODE_ENV || 'development',
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_ANALYTICS_ID: process.env.NEXT_PUBLIC_ANALYTICS_ID,
} as const
/**
* Validate required environment variables
*/
export function validateEnv() {
const missing: string[] = []
for (const [key, value] of Object.entries(requiredEnvVars)) {
if (!value) {
missing.push(key)
}
}
if (missing.length > 0) {
throw new Error(
`Missing required environment variables: ${missing.join(', ')}\n` +
'Please check your .env file or environment configuration.'
)
}
}
/**
* Get environment variable value
*/
export function getEnv(key: keyof typeof optionalEnvVars): string | undefined {
return optionalEnvVars[key]
}
/**
* Get all environment variables (for debugging)
*/
export function getAllEnv() {
return {
...requiredEnvVars,
...optionalEnvVars,
}
}
// Validate on module load in production
if (typeof window === 'undefined' && process.env.NODE_ENV === 'production') {
validateEnv()
}

92
src/lib/error-handler.ts Normal file
View File

@@ -0,0 +1,92 @@
/**
* Centralized error handling utilities
*/
import {
AppError,
NetworkError,
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
ServerError,
} from './types/errors'
/**
* Convert an unknown error to an AppError
*/
export function normalizeError(error: unknown): AppError {
if (error instanceof AppError) {
return error
}
if (error instanceof Error) {
// Check for network errors
if (error.message.includes('fetch') || error.message.includes('network')) {
return new NetworkError(error.message, error)
}
return new ServerError(error.message, error)
}
if (typeof error === 'string') {
return new ServerError(error)
}
return new ServerError('An unknown error occurred')
}
/**
* Handle API errors
*/
export function handleApiError(error: unknown): AppError {
const normalized = normalizeError(error)
// Log error for monitoring
if (typeof window !== 'undefined') {
console.error('API Error:', normalized)
// TODO: Send to error tracking service
}
return normalized
}
/**
* Get user-friendly error message
*/
export function getUserFriendlyMessage(error: AppError): string {
switch (error.code) {
case 'NETWORK_ERROR':
return 'Unable to connect to the server. Please check your internet connection and try again.'
case 'VALIDATION_ERROR':
return error.message || 'Please check your input and try again.'
case 'AUTH_ERROR':
return 'Your session has expired. Please log in again.'
case 'AUTHZ_ERROR':
return 'You do not have permission to perform this action.'
case 'NOT_FOUND':
return 'The requested resource was not found.'
case 'SERVER_ERROR':
return 'An error occurred on the server. Please try again later.'
default:
return error.message || 'An unexpected error occurred. Please try again.'
}
}
/**
* Check if error is retryable
*/
export function isRetryableError(error: AppError): boolean {
return (
error.code === 'NETWORK_ERROR' ||
(error.statusCode !== undefined && error.statusCode >= 500 && error.statusCode < 600)
)
}
/**
* Get retry delay in milliseconds
*/
export function getRetryDelay(attempt: number, baseDelay = 1000): number {
return Math.min(baseDelay * Math.pow(2, attempt), 30000) // Max 30 seconds
}

58
src/lib/graphql/client.ts Normal file
View File

@@ -0,0 +1,58 @@
import { ApolloClient, InMemoryCache, createHttpLink, from, ApolloLink } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { onError } from '@apollo/client/link/error'
import { handleApiError, getUserFriendlyMessage } from '@/lib/error-handler'
// HTTP Link - configured to use the GraphQL API
const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql',
})
// Auth Link - for adding authentication headers
const authLink = setContext((_, { headers }) => {
// Get the authentication token from secure storage
let token: string | null = null
if (typeof window !== 'undefined') {
// Try to get from secure storage
token = sessionStorage.getItem('auth_token')
}
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
}
})
// Error Link - handle GraphQL and network errors
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
const error = handleApiError(new Error(message))
console.error(
`[GraphQL error]: Message: ${getUserFriendlyMessage(error)}, Location: ${locations}, Path: ${path}`
)
})
}
if (networkError) {
const error = handleApiError(networkError)
console.error(`[Network error]: ${getUserFriendlyMessage(error)}`)
}
})
// Create Apollo Client
export const apolloClient = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
errorPolicy: 'all',
},
query: {
errorPolicy: 'all',
},
},
})

View File

@@ -0,0 +1,4 @@
export * from './useAuth'
export * from './useResources'
export * from './useSites'

View File

@@ -0,0 +1,46 @@
import { useMutation, useQuery } from '@apollo/client'
import { LOGIN, LOGOUT } from '../mutations'
import { GET_ME } from '../queries'
import { setAuthToken, removeAuthToken } from '@/lib/auth-storage'
export function useLogin() {
const [mutate, { loading, error }] = useMutation(LOGIN, {
onCompleted: (data) => {
if (data?.login?.token) {
setAuthToken(data.login.token)
}
},
})
return {
login: (email: string, password: string) =>
mutate({ variables: { email, password } }),
loading,
error,
}
}
export function useLogout() {
const [mutate, { loading, error }] = useMutation(LOGOUT, {
onCompleted: () => {
removeAuthToken()
// Optionally redirect to login page
if (typeof window !== 'undefined') {
window.location.href = '/login'
}
},
})
return {
logout: () => mutate(),
loading,
error,
}
}
export function useMe() {
return useQuery(GET_ME, {
fetchPolicy: 'cache-and-network',
})
}

View File

@@ -0,0 +1,53 @@
import { useQuery, useMutation, useSubscription } from '@apollo/client'
import { GET_RESOURCES, GET_RESOURCE, CREATE_RESOURCE, UPDATE_RESOURCE, DELETE_RESOURCE } from '../queries'
import { CREATE_RESOURCE as CREATE_RESOURCE_MUTATION, UPDATE_RESOURCE as UPDATE_RESOURCE_MUTATION, DELETE_RESOURCE as DELETE_RESOURCE_MUTATION } from '../mutations'
export function useResources(filter?: { type?: string; status?: string; siteId?: string }) {
return useQuery(GET_RESOURCES, {
variables: { filter },
fetchPolicy: 'cache-and-network',
})
}
export function useResource(id: string) {
return useQuery(GET_RESOURCE, {
variables: { id },
skip: !id,
})
}
export function useCreateResource() {
const [mutate, { loading, error }] = useMutation(CREATE_RESOURCE_MUTATION, {
refetchQueries: [{ query: GET_RESOURCES }],
})
return {
createResource: mutate,
loading,
error,
}
}
export function useUpdateResource() {
const [mutate, { loading, error }] = useMutation(UPDATE_RESOURCE_MUTATION, {
refetchQueries: [{ query: GET_RESOURCES }],
})
return {
updateResource: mutate,
loading,
error,
}
}
export function useDeleteResource() {
const [mutate, { loading, error }] = useMutation(DELETE_RESOURCE_MUTATION, {
refetchQueries: [{ query: GET_RESOURCES }],
})
return {
deleteResource: mutate,
loading,
error,
}
}

View File

@@ -0,0 +1,16 @@
import { useQuery } from '@apollo/client'
import { GET_SITES, GET_SITE } from '../queries'
export function useSites() {
return useQuery(GET_SITES, {
fetchPolicy: 'cache-and-network',
})
}
export function useSite(id: string) {
return useQuery(GET_SITE, {
variables: { id },
skip: !id,
})
}

View File

@@ -0,0 +1,89 @@
import { gql } from '@apollo/client'
export const LOGIN = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
user {
id
email
name
role
}
}
}
`
export const LOGOUT = gql`
mutation Logout {
logout
}
`
export const CREATE_RESOURCE = gql`
mutation CreateResource($input: CreateResourceInput!) {
createResource(input: $input) {
id
name
type
status
site {
id
name
}
metadata
createdAt
updatedAt
}
}
`
export const UPDATE_RESOURCE = gql`
mutation UpdateResource($id: ID!, $input: UpdateResourceInput!) {
updateResource(id: $id, input: $input) {
id
name
type
status
metadata
updatedAt
}
}
`
export const DELETE_RESOURCE = gql`
mutation DeleteResource($id: ID!) {
deleteResource(id: $id)
}
`
export const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
email
name
role
createdAt
}
}
`
export const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
email
name
role
updatedAt
}
}
`
export const DELETE_USER = gql`
mutation DeleteUser($id: ID!) {
deleteUser(id: $id)
}
`

113
src/lib/graphql/queries.ts Normal file
View File

@@ -0,0 +1,113 @@
import { gql } from '@apollo/client'
export const GET_RESOURCES = gql`
query GetResources($filter: ResourceFilter) {
resources(filter: $filter) {
id
name
type
status
site {
id
name
region
}
metadata
createdAt
updatedAt
}
}
`
export const GET_RESOURCE = gql`
query GetResource($id: ID!) {
resource(id: $id) {
id
name
type
status
site {
id
name
region
}
metadata
createdAt
updatedAt
}
}
`
export const GET_SITES = gql`
query GetSites {
sites {
id
name
region
status
metadata
createdAt
updatedAt
}
}
`
export const GET_SITE = gql`
query GetSite($id: ID!) {
site(id: $id) {
id
name
region
status
resources {
id
name
type
status
}
metadata
createdAt
updatedAt
}
}
`
export const GET_ME = gql`
query GetMe {
me {
id
email
name
role
createdAt
updatedAt
}
}
`
export const GET_USERS = gql`
query GetUsers {
users {
id
email
name
role
createdAt
updatedAt
}
}
`
export const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
email
name
role
createdAt
updatedAt
}
}
`

View File

@@ -0,0 +1,31 @@
import { gql } from '@apollo/client'
// Query for getting metrics
export const GET_METRICS = gql`
query GetMetrics($resourceId: ID!, $metricType: MetricType!, $timeRange: TimeRange!) {
metrics(resourceId: $resourceId, metricType: $metricType, timeRange: $timeRange) {
resource {
id
name
}
metricType
values {
timestamp
value
labels
}
timeRange {
start
end
}
}
}
`
// Query for getting health status
export const GET_HEALTH = gql`
query GetHealth($resourceId: ID!) {
health(resourceId: $resourceId)
}
`

View File

@@ -0,0 +1,104 @@
import { gql } from '@apollo/client'
// Query for getting all regions
export const GET_REGIONS = gql`
query GetRegions {
regions {
id
name
code
country
coordinates {
latitude
longitude
}
sites {
id
name
}
clusters {
id
name
}
}
}
`
// Query for getting a specific region
export const GET_REGION = gql`
query GetRegion($id: ID!) {
region(id: $id) {
id
name
code
country
coordinates {
latitude
longitude
}
sites {
id
name
clusters {
id
name
}
}
clusters {
id
name
nodes {
id
name
health
}
}
}
}
`
// Query for getting resources
export const GET_RESOURCES = gql`
query GetResources($filter: ResourceFilter) {
resources(filter: $filter) {
id
name
type
region {
id
name
}
health
metadata
}
}
`
// Query for getting network topology
export const GET_NETWORK_TOPOLOGY = gql`
query GetNetworkTopology($regionId: ID) {
networkTopology(regionId: $regionId) {
nodes {
id
name
type
position {
x
y
z
}
health
}
edges {
id
from
to
type
latency
bandwidth
status
}
}
}
`

View File

@@ -0,0 +1,104 @@
import { gql } from '@apollo/client'
// Query for getting all pillars
export const GET_PILLARS = gql`
query GetPillars {
pillars {
id
code
name
description
controls {
id
code
name
description
}
}
}
`
// Query for getting a specific pillar
export const GET_PILLAR = gql`
query GetPillar($code: PillarCode!) {
pillar(code: $code) {
id
code
name
description
controls {
id
code
name
description
findings {
id
resource {
id
name
}
status
severity
title
description
recommendation
}
}
}
}
`
// Query for getting findings
export const GET_FINDINGS = gql`
query GetFindings($filter: FindingFilter) {
findings(filter: $filter) {
id
control {
id
code
name
pillar {
code
name
}
}
resource {
id
name
type
}
status
severity
title
description
recommendation
createdAt
updatedAt
}
}
`
// Query for getting risks
export const GET_RISKS = gql`
query GetRisks($resourceId: ID) {
risks(resourceId: $resourceId) {
id
resource {
id
name
type
}
pillar {
code
name
}
severity
title
description
mitigation
createdAt
updatedAt
}
}
`

View File

@@ -0,0 +1,153 @@
/**
* GraphQL TypeScript types
* These types should match the GraphQL schema
*/
export type ResourceType =
| 'REGION'
| 'SITE'
| 'CLUSTER'
| 'NODE'
| 'VM'
| 'POD'
| 'SERVICE'
| 'NETWORK'
| 'STORAGE'
| 'TUNNEL'
| 'POLICY'
export type PillarCode =
| 'SECURITY'
| 'RELIABILITY'
| 'COST_OPTIMIZATION'
| 'PERFORMANCE_EFFICIENCY'
| 'OPERATIONAL_EXCELLENCE'
| 'SUSTAINABILITY'
export type HealthStatus = 'HEALTHY' | 'DEGRADED' | 'UNHEALTHY' | 'UNKNOWN'
export type FindingStatus = 'PASS' | 'FAIL' | 'WARNING' | 'INFO' | 'NOT_APPLICABLE'
export type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'INFO'
export type MetricType =
| 'CPU_USAGE'
| 'MEMORY_USAGE'
| 'NETWORK_THROUGHPUT'
| 'NETWORK_LATENCY'
| 'STORAGE_IOPS'
| 'REQUEST_RATE'
| 'ERROR_RATE'
| 'COST'
| 'HEALTH_SCORE'
export interface Region {
id: string
name: string
code: string
country?: string
coordinates?: {
latitude: number
longitude: number
}
sites?: Site[]
clusters?: Cluster[]
}
export interface Site {
id: string
name: string
region: Region
clusters?: Cluster[]
}
export interface Cluster {
id: string
name: string
region?: Region
site?: Site
nodes?: Node[]
services?: Service[]
health: HealthStatus
}
export interface Node {
id: string
name: string
cluster: Cluster
health: HealthStatus
}
export interface Service {
id: string
name: string
cluster?: Cluster
dependencies?: Service[]
health: HealthStatus
}
export interface Resource {
id: string
name: string
type: ResourceType
region?: Region
health?: HealthStatus
metadata?: Record<string, unknown>
}
export interface Pillar {
id: string
code: PillarCode
name: string
description?: string
controls?: Control[]
}
export interface Control {
id: string
code: string
name: string
pillar: Pillar
description?: string
findings?: Finding[]
}
export interface Finding {
id: string
control: Control
resource: Resource
status: FindingStatus
severity: Severity
title: string
description?: string
recommendation?: string
}
export interface Risk {
id: string
resource: Resource
pillar?: Pillar
severity: Severity
title: string
description?: string
mitigation?: string
}
export interface Metrics {
resource: Resource
metricType: MetricType
values: MetricValue[]
timeRange: TimeRange
}
export interface MetricValue {
timestamp: string
value: number
labels?: Record<string, string>
}
export interface TimeRange {
start: string
end: string
}

View File

@@ -0,0 +1,75 @@
import { gql } from '@apollo/client'
// Subscription for resource updates
export const RESOURCE_UPDATED = gql`
subscription ResourceUpdated($resourceId: ID!) {
resourceUpdated(resourceId: $resourceId) {
id
name
type
health
metadata
}
}
`
// Subscription for metrics updates
export const METRICS_UPDATED = gql`
subscription MetricsUpdated($resourceId: ID!, $metricType: MetricType!) {
metricsUpdated(resourceId: $resourceId, metricType: $metricType) {
timestamp
value
labels
}
}
`
// Subscription for health changes
export const HEALTH_CHANGED = gql`
subscription HealthChanged($resourceId: ID!) {
healthChanged(resourceId: $resourceId)
}
`
// Subscription for new findings
export const FINDING_CREATED = gql`
subscription FindingCreated($controlId: ID) {
findingCreated(controlId: $controlId) {
id
control {
id
code
name
}
resource {
id
name
}
status
severity
title
description
}
}
`
// Subscription for new risks
export const RISK_CREATED = gql`
subscription RiskCreated($resourceId: ID) {
riskCreated(resourceId: $resourceId) {
id
resource {
id
name
}
pillar {
code
name
}
severity
title
description
}
}
`

94
src/lib/logger.ts Normal file
View File

@@ -0,0 +1,94 @@
/**
* Structured logging utility
*/
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
interface LogEntry {
level: LogLevel
message: string
timestamp: string
context?: Record<string, unknown>
error?: Error
}
class Logger {
private logLevel: LogLevel
constructor() {
this.logLevel = (process.env.NODE_ENV === 'development' ? 'debug' : 'info') as LogLevel
}
private shouldLog(level: LogLevel): boolean {
const levels: LogLevel[] = ['debug', 'info', 'warn', 'error']
return levels.indexOf(level) >= levels.indexOf(this.logLevel)
}
private formatEntry(entry: LogEntry): string {
const { level, message, timestamp, context, error } = entry
const parts = [`[${timestamp}]`, `[${level.toUpperCase()}]`, message]
if (context && Object.keys(context).length > 0) {
parts.push(JSON.stringify(context))
}
if (error) {
parts.push(`\nError: ${error.message}`)
if (error.stack) {
parts.push(`\nStack: ${error.stack}`)
}
}
return parts.join(' ')
}
private log(level: LogLevel, message: string, context?: Record<string, unknown>, error?: Error) {
if (!this.shouldLog(level)) {
return
}
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
context,
error,
}
const formatted = this.formatEntry(entry)
switch (level) {
case 'debug':
console.debug(formatted)
break
case 'info':
console.info(formatted)
break
case 'warn':
console.warn(formatted)
break
case 'error':
console.error(formatted)
break
}
}
debug(message: string, context?: Record<string, unknown>) {
this.log('debug', message, context)
}
info(message: string, context?: Record<string, unknown>) {
this.log('info', message, context)
}
warn(message: string, context?: Record<string, unknown>) {
this.log('warn', message, context)
}
error(message: string, error?: Error, context?: Record<string, unknown>) {
this.log('error', message, context, error)
}
}
export const logger = new Logger()

87
src/lib/monitoring.ts Normal file
View File

@@ -0,0 +1,87 @@
/**
* Application monitoring and error tracking
*/
let sentryInitialized = false
/**
* Initialize Sentry for error tracking
*/
export function initMonitoring() {
if (typeof window === 'undefined') {
return
}
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN
if (!dsn || sentryInitialized) {
return
}
// Initialize Sentry in production
if (process.env.NODE_ENV === 'production') {
// Sentry will be initialized via sentry.client.config.ts
sentryInitialized = true
}
}
/**
* Log error to monitoring service
*/
export function logError(error: Error, context?: Record<string, unknown>) {
if (typeof window === 'undefined') {
console.error('Error:', error, context)
return
}
// In production, this will send to Sentry
if (process.env.NODE_ENV === 'production' && typeof window !== 'undefined') {
// @ts-expect-error - Sentry will be available
if (window.Sentry) {
// @ts-expect-error
window.Sentry.captureException(error, { extra: context })
}
} else {
console.error('Error:', error, context)
}
}
/**
* Log performance metric
*/
export function logPerformance(name: string, duration: number, metadata?: Record<string, unknown>) {
if (typeof window === 'undefined') {
return
}
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.log(`Performance: ${name} took ${duration}ms`, metadata)
}
// In production, send to monitoring service
if (process.env.NODE_ENV === 'production') {
// @ts-expect-error - Sentry will be available
if (window.Sentry) {
// @ts-expect-error
window.Sentry.metrics.distribution(name, duration, { tags: metadata })
}
}
}
/**
* Track custom event
*/
export function trackEvent(name: string, properties?: Record<string, unknown>) {
if (typeof window === 'undefined') {
return
}
// In production, send to analytics
if (process.env.NODE_ENV === 'production') {
// Add analytics tracking here
console.log('Event:', name, properties)
} else {
console.log('Event:', name, properties)
}
}

81
src/lib/performance.ts Normal file
View File

@@ -0,0 +1,81 @@
/**
* Performance monitoring and optimization utilities
*/
/**
* Measure performance of a function
*/
export async function measurePerformance<T>(
name: string,
fn: () => Promise<T> | T
): Promise<T> {
const start = performance.now()
try {
const result = await fn()
const duration = performance.now() - start
if (typeof window !== 'undefined') {
const { logPerformance } = await import('./monitoring')
logPerformance(name, duration)
}
return result
} catch (error) {
const duration = performance.now() - start
console.error(`Performance error for ${name}:`, error, `(took ${duration}ms)`)
throw error
}
}
/**
* Lazy load a component
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function lazyLoad<T extends React.ComponentType<any>>(
importFn: () => Promise<{ default: T }>
) {
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const React = require('react')
return React.lazy(importFn)
}
/**
* Debounce function
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null
return function executedFunction(...args: Parameters<T>) {
const later = () => {
timeout = null
func(...args)
}
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(later, wait)
}
}
/**
* Throttle function
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean
return function executedFunction(...args: Parameters<T>) {
if (!inThrottle) {
func(...args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}

30
src/lib/test-helpers.ts Normal file
View File

@@ -0,0 +1,30 @@
/**
* Test helper utilities
*/
/**
* Mock window.matchMedia
*/
export function mockMatchMedia() {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
})
}
/**
* Wait for async operations to complete
*/
export function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

9
src/lib/test-setup.ts Normal file
View File

@@ -0,0 +1,9 @@
import '@testing-library/jest-dom'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'
// Cleanup after each test
afterEach(() => {
cleanup()
})

37
src/lib/test-utils.tsx Normal file
View File

@@ -0,0 +1,37 @@
import React, { ReactElement } from 'react'
import { render, RenderOptions } from '@testing-library/react'
import { ApolloProvider } from '@apollo/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { apolloClient } from './graphql/client'
// Create a test query client
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
})
interface AllTheProvidersProps {
children: React.ReactNode
}
const AllTheProviders = ({ children }: AllTheProvidersProps) => {
const queryClient = createTestQueryClient()
return (
<ApolloProvider client={apolloClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ApolloProvider>
)
}
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) =>
render(ui, { wrapper: AllTheProviders, ...options })
export * from '@testing-library/react'
export { customRender as render }

65
src/lib/types/errors.ts Normal file
View File

@@ -0,0 +1,65 @@
/**
* Error types and utilities
*/
export class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode?: number,
public cause?: Error
) {
super(message)
this.name = 'AppError'
Object.setPrototypeOf(this, AppError.prototype)
}
}
export class NetworkError extends AppError {
constructor(message: string, cause?: Error) {
super(message, 'NETWORK_ERROR', 0, cause)
this.name = 'NetworkError'
Object.setPrototypeOf(this, NetworkError.prototype)
}
}
export class ValidationError extends AppError {
constructor(message: string, public fields?: Record<string, string[]>) {
super(message, 'VALIDATION_ERROR', 400)
this.name = 'ValidationError'
Object.setPrototypeOf(this, ValidationError.prototype)
}
}
export class AuthenticationError extends AppError {
constructor(message: string = 'Authentication required') {
super(message, 'AUTH_ERROR', 401)
this.name = 'AuthenticationError'
Object.setPrototypeOf(this, AuthenticationError.prototype)
}
}
export class AuthorizationError extends AppError {
constructor(message: string = 'Insufficient permissions') {
super(message, 'AUTHZ_ERROR', 403)
this.name = 'AuthorizationError'
Object.setPrototypeOf(this, AuthorizationError.prototype)
}
}
export class NotFoundError extends AppError {
constructor(message: string = 'Resource not found') {
super(message, 'NOT_FOUND', 404)
this.name = 'NotFoundError'
Object.setPrototypeOf(this, NotFoundError.prototype)
}
}
export class ServerError extends AppError {
constructor(message: string = 'Internal server error', cause?: Error) {
super(message, 'SERVER_ERROR', 500, cause)
this.name = 'ServerError'
Object.setPrototypeOf(this, ServerError.prototype)
}
}

22
src/lib/utils.test.ts Normal file
View File

@@ -0,0 +1,22 @@
import { describe, it, expect } from 'vitest'
import { cn } from './utils'
describe('cn utility', () => {
it('should merge class names', () => {
expect(cn('foo', 'bar')).toBe('foo bar')
})
it('should handle conditional classes', () => {
expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz')
})
it('should merge Tailwind classes correctly', () => {
expect(cn('p-4 p-2')).toBe('p-2')
})
it('should handle empty inputs', () => {
expect(cn()).toBe('')
expect(cn('')).toBe('')
})
})

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

@@ -0,0 +1,10 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
/**
* Utility function to merge Tailwind CSS classes
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,82 @@
/**
* Zod validation schemas
*/
import { z } from 'zod'
/**
* Common validation schemas
*/
export const emailSchema = z.string().email('Invalid email address')
export const urlSchema = z.string().url('Invalid URL')
export const nonEmptyStringSchema = z.string().min(1, 'This field is required')
/**
* Resource validation schemas
*/
export const resourceNameSchema = z
.string()
.min(1, 'Resource name is required')
.max(63, 'Resource name must be 63 characters or less')
.regex(
/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/,
'Resource name must be lowercase alphanumeric with hyphens'
)
export const memorySchema = z
.string()
.regex(/^\d+[KMGT]i?$/, 'Memory must be in format like 4Gi, 8Mi, etc.')
export const diskSizeSchema = z
.string()
.regex(/^\d+[KMGT]i?$/, 'Disk size must be in format like 50Gi, 100Mi, etc.')
export const cpuSchema = z.number().int().min(1).max(128)
/**
* VM creation schema
*/
export const vmCreateSchema = z.object({
name: resourceNameSchema,
node: nonEmptyStringSchema,
cpu: cpuSchema,
memory: memorySchema,
disk: diskSizeSchema,
storage: nonEmptyStringSchema,
network: nonEmptyStringSchema,
image: nonEmptyStringSchema,
site: nonEmptyStringSchema,
userData: z.string().optional(),
sshKeys: z.array(z.string()).optional(),
})
export type VMCreateInput = z.infer<typeof vmCreateSchema>
/**
* Form validation utilities
*/
export function validateForm<T>(schema: z.ZodSchema<T>, data: unknown): {
success: boolean
data?: T
errors?: Record<string, string[]>
} {
const result = schema.safeParse(data)
if (result.success) {
return { success: true, data: result.data }
}
const errors: Record<string, string[]> = {}
result.error.errors.forEach((error) => {
const path = error.path.join('.')
if (!errors[path]) {
errors[path] = []
}
errors[path].push(error.message)
})
return { success: false, errors }
}

74
src/types/index.ts Normal file
View File

@@ -0,0 +1,74 @@
/**
* Shared TypeScript type definitions
*/
// API Types
export interface Resource {
id: string
name: string
type: ResourceType
status: ResourceStatus
site: Site
metadata?: Record<string, unknown>
createdAt: string
updatedAt: string
}
export interface Site {
id: string
name: string
region: string
status: SiteStatus
resources?: Resource[]
metadata?: Record<string, unknown>
createdAt: string
updatedAt: string
}
export interface User {
id: string
email: string
name: string
role: UserRole
createdAt: string
updatedAt: string
}
export type ResourceType = 'VM' | 'CONTAINER' | 'STORAGE' | 'NETWORK'
export type ResourceStatus = 'PENDING' | 'PROVISIONING' | 'RUNNING' | 'STOPPED' | 'ERROR' | 'DELETING'
export type SiteStatus = 'ACTIVE' | 'INACTIVE' | 'MAINTENANCE'
export type UserRole = 'ADMIN' | 'USER' | 'VIEWER'
// Form Types
export interface ResourceFormData {
name: string
type: ResourceType
siteId: string
metadata?: Record<string, unknown>
}
// API Response Types
export interface ApiResponse<T> {
data?: T
errors?: Array<{
message: string
code?: string
path?: string[]
}>
}
// Pagination Types
export interface PaginationParams {
page?: number
limit?: number
offset?: number
}
export interface PaginatedResponse<T> {
items: T[]
total: number
page: number
limit: number
hasMore: boolean
}