feat (v1.0.0): initial refactor and redesign
This commit is contained in:
parent
3058aa1ab4
commit
fe9b50b30e
134 changed files with 17792 additions and 3670 deletions
72
components/ui/Card.tsx
Normal file
72
components/ui/Card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
components/ui/CardGrid.tsx
Normal file
41
components/ui/CardGrid.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
95
components/ui/PaginatedCardList.tsx
Normal file
95
components/ui/PaginatedCardList.tsx
Normal 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
32
components/ui/Section.tsx
Normal 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
20
components/ui/Surface.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue