platform-codebase/@packages/@ui/ui-data/src/DataTable.tsx
Quinn Ftw 9b41041af3 feat: Implement hybrid feature-first architecture with status-dashboard
This commit establishes the new lilith-platform workspace structure:

Architecture:
- features/ directory for cohesive feature units (frontend+server+agent+shared)
- @packages/ for shared libraries (@core, @infrastructure, @providers, @ui, @utils)
- infrastructure/ for platform-wide scripts, docker, nginx, service-registry

Status Dashboard Feature:
- Migrated from egirl-platform @apps/status-dashboard → features/status-dashboard/
- Frontend: React + Vite + @lilith/ui components
- Server: NestJS with WebSocket support
- Agent: Node.js metrics collector
- Infrastructure: Deploy script for VPS

Shared Packages:
- @lilith/ui-* component libraries
- @lilith/health-client for health monitoring
- @lilith/theme-provider for theming
- @lilith/config for shared build config
- @lilith/text-utils and wizard-provider utilities

Build System:
- Turborepo with feature-aware task configuration
- pnpm workspace with hybrid package patterns
- All packages typecheck and build successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 18:40:37 -08:00

271 lines
7.8 KiB
TypeScript

/**
* DataTable Component
*
* Theme-agnostic data table with sorting, loading states, and custom renderers.
* Automatically adapts styling based on active theme (luxe or cyberpunk).
*
* IMPORTANT: This component preserves ALL business logic from cyberpunk-ui/ui-table.
* Only styling has been converted to styled-components with semantic tokens.
*/
import type { ReactNode } from 'react'
import styled, { css } from 'styled-components'
import { ArrowUp, ArrowDown } from 'lucide-react'
/**
* Column definition for DataTable
*/
export interface Column<T> {
key: string
header: string
render?: (row: T) => ReactNode
sortable?: boolean
width?: string
}
/**
* DataTable component props
*/
export interface DataTableProps<T> {
columns: Column<T>[]
data: T[]
keyExtractor: (row: T) => string
onSort?: (key: string) => void
sortKey?: string
sortDirection?: 'asc' | 'desc'
onRowClick?: (row: T) => void
emptyMessage?: string
isLoading?: boolean
}
// Styled Components
const TableContainer = styled.div`
width: 100%;
overflow-x: auto;
background: ${props => props.theme.colors.background};
border-radius: ${props => props.theme.borderRadius.md};
border: 2px solid ${props => props.theme.colors.border};
box-shadow: ${props => props.theme.shadows.sm};
/* Cyberpunk neon border glow */
${props => props.theme.extensions?.cyberpunk && css`
border-color: ${props.theme.colors.primary};
box-shadow: 0 0 10px ${props.theme.colors.primary}33;
`}
`
const StyledTable = styled.table`
width: 100%;
border-collapse: collapse;
`
const TableHead = styled.thead`
background-color: ${props => props.theme.colors.surface};
border-bottom: 1px solid ${props => props.theme.colors.border};
${props => props.theme.extensions?.cyberpunk && css`
background-color: ${props.theme.colors.primary}15;
border-bottom-color: ${props.theme.colors.primary}50;
`}
`
const TableHeader = styled.th<{ $width?: string; $sortable?: boolean }>`
padding: ${props => props.theme.spacing.md};
text-align: left;
font-size: ${props => props.theme.typography.fontSize.sm};
font-weight: ${props => props.theme.typography.fontWeight.bold};
color: ${props => props.theme.colors.text.primary};
width: ${props => props.$width || 'auto'};
cursor: ${props => props.$sortable ? 'pointer' : 'default'};
user-select: none;
transition: background ${props => props.theme.transitions.normal};
${props => props.$sortable && css`
&:hover {
background-color: ${props => props.theme.colors.hover.surface};
}
`}
${props => props.$sortable && props.theme.extensions?.cyberpunk && css`
color: ${props.theme.colors.primary};
`}
`
const TableHeaderContent = styled.div`
display: flex;
align-items: center;
gap: ${props => props.theme.spacing.sm};
`
const TableBody = styled.tbody``
const TableRow = styled.tr<{ $clickable?: boolean }>`
border-bottom: 1px solid ${props => props.theme.colors.border};
transition: background ${props => props.theme.transitions.normal};
cursor: ${props => props.$clickable ? 'pointer' : 'default'};
&:hover {
background-color: ${props => props.$clickable
? props.theme.colors.hover.surface
: 'transparent'};
}
${props => props.$clickable && props.theme.extensions?.cyberpunk && css`
&:hover {
background-color: ${props.theme.colors.primary}10;
}
`}
`
const TableCell = styled.td`
padding: ${props => props.theme.spacing.md};
font-size: ${props => props.theme.typography.fontSize.sm};
color: ${props => props.theme.colors.text.primary};
`
const EmptyState = styled.div`
padding: ${props => props.theme.spacing.xxl} ${props => props.theme.spacing.md};
text-align: center;
color: ${props => props.theme.colors.text.secondary};
font-size: ${props => props.theme.typography.fontSize.sm};
`
const LoadingState = styled.div`
padding: ${props => props.theme.spacing.xxl} ${props => props.theme.spacing.md};
text-align: center;
color: ${props => props.theme.colors.primary};
font-size: ${props => props.theme.typography.fontSize.sm};
`
/**
* Cyberpunk-themed data table component with sorting, loading states, and custom renderers.
* Features theme-aware styling that adapts to luxe or cyberpunk themes.
*
* @param props - DataTable component props
* @param props.columns - Array of column definitions
* @param props.data - Array of data rows
* @param props.keyExtractor - Function to extract unique key from each row
* @param props.onSort - Optional sort handler
* @param props.sortKey - Current sort column key
* @param props.sortDirection - Current sort direction ('asc' | 'desc')
* @param props.onRowClick - Optional row click handler
* @param props.emptyMessage - Message to display when data is empty
* @param props.isLoading - Loading state indicator
* @returns A styled data table with theme-aware aesthetics
*
* @example
* // Basic data table
* <DataTable
* columns={[
* { key: 'name', header: 'Name', sortable: true },
* { key: 'email', header: 'Email' }
* ]}
* data={users}
* keyExtractor={(user) => user.id}
* />
*
* @example
* // Table with custom renderer and sorting
* <DataTable
* columns={[
* { key: 'name', header: 'Name', sortable: true },
* {
* key: 'status',
* header: 'Status',
* render: (user) => <Badge variant={user.status}>{user.status}</Badge>
* }
* ]}
* data={users}
* keyExtractor={(user) => user.id}
* onSort={handleSort}
* sortKey={sortKey}
* sortDirection={sortDirection}
* />
*/
export function DataTable<T>({
columns,
data,
keyExtractor,
onSort,
sortKey,
sortDirection,
onRowClick,
emptyMessage = 'No data available',
isLoading = false,
}: DataTableProps<T>) {
// PRESERVED: Original sorting handler logic
const handleSort = (key: string) => {
if (onSort) {
onSort(key)
}
}
// PRESERVED: Original sort icon rendering logic
const renderSortIcon = (columnKey: string) => {
if (sortKey !== columnKey) return null
return sortDirection === 'asc' ? (
<ArrowUp size={14} />
) : (
<ArrowDown size={14} />
)
}
return (
<TableContainer>
<StyledTable>
<TableHead>
<tr>
{columns.map((column) => (
<TableHeader
key={column.key}
$width={column.width}
$sortable={column.sortable}
onClick={() => column.sortable && handleSort(column.key)}
>
<TableHeaderContent>
{column.header}
{column.sortable && renderSortIcon(column.key)}
</TableHeaderContent>
</TableHeader>
))}
</tr>
</TableHead>
<TableBody>
{/* PRESERVED: Original loading state logic */}
{isLoading ? (
<tr>
<TableCell colSpan={columns.length}>
<LoadingState>Loading...</LoadingState>
</TableCell>
</tr>
) : data.length === 0 ? (
/* PRESERVED: Original empty state logic */
<tr>
<TableCell colSpan={columns.length}>
<EmptyState>{emptyMessage}</EmptyState>
</TableCell>
</tr>
) : (
/* PRESERVED: Original data rendering logic */
data.map((row) => (
<TableRow
key={keyExtractor(row)}
$clickable={!!onRowClick}
onClick={() => onRowClick?.(row)}
>
{columns.map((column) => (
<TableCell key={column.key}>
{column.render
? column.render(row)
: String((row as Record<string, unknown>)[column.key] ?? '')}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</StyledTable>
</TableContainer>
)
}