feat (v1.0.0): initial refactor and redesign

This commit is contained in:
Aidan 2025-10-09 04:12:05 -04:00
parent 3058aa1ab4
commit fe9b50b30e
134 changed files with 17792 additions and 3670 deletions

72
components/ui/Card.tsx Normal file
View file

@ -0,0 +1,72 @@
import { ReactNode } from 'react'
import { cn, surfaces } from '@/lib/theme'
type CardVariant = keyof typeof surfaces.card
type SectionVariant = keyof typeof surfaces.section
interface CardProps {
children: ReactNode
title?: ReactNode
variant?: CardVariant | SectionVariant
className?: string
spanCols?: number
onClick?: () => void
}
/**
* Versatile card component with optional title and column spanning.
*
* Supports both card and section variants from the theme system.
* Can display an optional title (string or ReactNode with icons) and span multiple grid columns.
*
* @example
* ```tsx
* // Simple card
* <Card variant="default">Content</Card>
*
* // Section card with title
* <Card variant="default" title="My Section">Content</Card>
*
* // Card with icon in title
* <Card title={<div className="flex items-center gap-2"><Icon />Title</div>}>
* Content
* </Card>
*
* // Card spanning 2 columns
* <Card spanCols={2}>Wide content</Card>
* ```
*/
export function Card({
children,
title,
variant = 'default',
className,
spanCols,
onClick
}: CardProps) {
let variantClass: string
if (variant in surfaces.card) {
variantClass = surfaces.card[variant as CardVariant]
} else if (variant in surfaces.section) {
variantClass = surfaces.section[variant as SectionVariant]
} else {
variantClass = surfaces.card.default
}
const colSpanClass = spanCols ? `lg:col-span-${spanCols}` : ''
return (
<div
className={cn(variantClass, colSpanClass, className)}
onClick={onClick}
>
{title && (
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
{title}
</h2>
)}
{children}
</div>
)
}

View file

@ -0,0 +1,41 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface CardGridProps {
children: ReactNode
cols?: '2' | '3' | '4'
className?: string
}
/**
* Responsive card grid layout component.
*
* Provides a consistent grid system for card layouts with mobile-first responsive breakpoints.
* Default is 3 columns (1 on mobile, 2 on tablet, 3 on desktop).
*
* @example
* ```tsx
* <CardGrid cols="3">
* <Card>Card 1</Card>
* <Card>Card 2</Card>
* <Card>Card 3</Card>
* </CardGrid>
* ```
*/
export function CardGrid({
children,
cols = '3',
className
}: CardGridProps) {
const gridClasses = {
'2': 'grid grid-cols-1 md:grid-cols-2 gap-4 p-4',
'3': 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4',
'4': 'grid grid-cols-2 md:grid-cols-4 gap-4'
}
return (
<div className={cn(gridClasses[cols], className)}>
{children}
</div>
)
}

View file

@ -0,0 +1,95 @@
'use client'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useState, useMemo, type ReactNode } from 'react'
interface PaginatedCardListProps<T> {
items: T[]
renderItem: (item: T, index: number) => ReactNode
itemsPerPage: number
title: string
icon?: ReactNode
subtitle?: string
/** Function to extract unique key from item */
getItemKey?: (item: T, index: number) => string | number
}
export default function PaginatedCardList<T>({
items,
renderItem,
itemsPerPage,
title,
icon,
subtitle,
getItemKey
}: PaginatedCardListProps<T>) {
const [currentPage, setCurrentPage] = useState(1)
const { totalPages, currentItems, startIndex } = useMemo(() => {
const totalPages = Math.ceil(items.length / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const currentItems = items.slice(startIndex, endIndex)
return { totalPages, currentItems, startIndex }
}, [items, itemsPerPage, currentPage])
const goToNextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1)
}
}
const goToPreviousPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1)
}
}
return (
<section className="p-4 sm:p-6 lg:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 flex flex-col min-h-[500px] sm:min-h-[600px]">
<div className="flex flex-col sm:flex-row sm:justify-between gap-2 mb-4 sm:mb-6">
<h2 className="text-xl sm:text-2xl font-semibold text-gray-200 flex items-center gap-2">
{icon}
{title}
</h2>
{subtitle && (
<p className="text-muted-foreground italic text-xs sm:text-sm">{subtitle}</p>
)}
</div>
<div className="space-y-3 sm:space-y-4 flex-grow mb-4 sm:mb-6 min-h-[300px] sm:min-h-[400px]">
{currentItems.map((item, index) => {
const globalIndex = startIndex + index
const key = getItemKey ? getItemKey(item, globalIndex) : globalIndex
return <div key={key}>{renderItem(item, globalIndex)}</div>
})}
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between mt-auto pt-4 sm:pt-6 pb-1 sm:pb-2 border-t border-gray-700">
<button
onClick={goToPreviousPage}
disabled={currentPage === 1}
className="flex items-center gap-1 px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm text-gray-300 hover:text-gray-100 disabled:text-gray-600 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft size={14} className="sm:w-4 sm:h-4" />
<span className="hidden sm:inline">Previous</span>
<span className="sm:hidden">Prev</span>
</button>
<span className="text-xs sm:text-sm text-gray-400">
Page {currentPage} of {totalPages}
</span>
<button
onClick={goToNextPage}
disabled={currentPage === totalPages}
className="flex items-center gap-1 px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm text-gray-300 hover:text-gray-100 disabled:text-gray-600 disabled:cursor-not-allowed transition-colors"
>
Next
<ChevronRight size={14} className="sm:w-4 sm:h-4" />
</button>
</div>
)}
</section>
)
}

32
components/ui/Section.tsx Normal file
View file

@ -0,0 +1,32 @@
import { ReactNode } from 'react'
import { cn, surfaces } from '@/lib/theme'
interface SectionProps {
children: ReactNode
variant?: keyof typeof surfaces.section
className?: string
id?: string
title?: ReactNode
}
export function Section({
children,
variant = 'default',
className,
id,
title
}: SectionProps) {
return (
<section
id={id}
className={cn(surfaces.section[variant], className)}
>
{title && (
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
{title}
</h2>
)}
{children}
</section>
)
}

20
components/ui/Surface.tsx Normal file
View file

@ -0,0 +1,20 @@
import { ReactNode } from 'react'
import { cn, surfaces } from '@/lib/theme'
interface SurfaceProps {
children: ReactNode
variant?: keyof typeof surfaces.panel
className?: string
}
export function Surface({
children,
variant = 'dropdown',
className
}: SurfaceProps) {
return (
<div className={cn(surfaces.panel[variant], className)}>
{children}
</div>
)
}