953f598b9b
Componentes React reutilizables: card, dialog, tabs, select, alert, badge, button, input, label, skeleton, tooltip, progress_bar, page_header, form_field, settings_page, crud_page, analytics_page, dashboard_layout. Charts: area, bar, line, sparkline, kpi_card, chart_container. Hooks Wails: use_wails_query, use_wails_mutation, use_wails_stream, use_wails_event, use_animated_canvas. Funciones core: cn, format_compact, chart_colors, get_series_color, wails_cache, theme_config_to_colors. Tipos: chart_series, wails_ipc, theme_config, component_variants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
121 lines
4.5 KiB
TypeScript
121 lines
4.5 KiB
TypeScript
import * as React from 'react'
|
|
import { cn } from '../core/cn'
|
|
|
|
interface CrudField {
|
|
key: string
|
|
label: string
|
|
type: 'text' | 'number' | 'email' | 'select' | 'textarea'
|
|
required?: boolean
|
|
options?: Array<{ label: string; value: string }>
|
|
placeholder?: string
|
|
}
|
|
|
|
interface CrudPageProps<T extends Record<string, unknown>> {
|
|
title: string
|
|
subtitle?: string
|
|
data: T[]
|
|
fields: CrudField[]
|
|
columns: Array<{
|
|
key: keyof T
|
|
label: string
|
|
render?: (value: unknown, row: T) => React.ReactNode
|
|
}>
|
|
onAdd?: (item: Partial<T>) => void
|
|
onEdit?: (item: T) => void
|
|
onDelete?: (item: T) => void
|
|
actions?: React.ReactNode
|
|
className?: string
|
|
}
|
|
|
|
export function crudPage<T extends Record<string, unknown>>({
|
|
title,
|
|
subtitle,
|
|
data,
|
|
fields,
|
|
columns,
|
|
onAdd,
|
|
onEdit,
|
|
onDelete,
|
|
actions,
|
|
className,
|
|
}: CrudPageProps<T>): React.ReactElement {
|
|
return (
|
|
<div className={cn('space-y-6', className)}>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between border-b pb-4">
|
|
<div className="space-y-1">
|
|
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{actions}
|
|
{onAdd && (
|
|
<button className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground hover:bg-primary/80">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 5v14M5 12h14"/></svg>
|
|
Add {title.replace(/s$/, '')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="rounded-lg border">
|
|
<table className="w-full caption-bottom text-sm">
|
|
<thead className="border-b bg-muted/50">
|
|
<tr>
|
|
{columns.map((col) => (
|
|
<th key={String(col.key)} className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">
|
|
{col.label}
|
|
</th>
|
|
))}
|
|
{(onEdit || onDelete) && (
|
|
<th className="px-4 py-3 text-right text-sm font-medium text-muted-foreground">Actions</th>
|
|
)}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y">
|
|
{data.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={columns.length + (onEdit || onDelete ? 1 : 0)} className="h-24 text-center text-muted-foreground">
|
|
No items yet.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
data.map((row, i) => (
|
|
<tr key={i} className="hover:bg-muted/50">
|
|
{columns.map((col) => (
|
|
<td key={String(col.key)} className="px-4 py-3 align-middle">
|
|
{col.render ? col.render(row[col.key], row) : String(row[col.key] ?? '')}
|
|
</td>
|
|
))}
|
|
{(onEdit || onDelete) && (
|
|
<td className="px-4 py-3 text-right">
|
|
<div className="flex justify-end gap-1">
|
|
{onEdit && (
|
|
<button onClick={() => onEdit(row)} className="inline-flex size-7 items-center justify-center rounded-md hover:bg-muted">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
</button>
|
|
)}
|
|
{onDelete && (
|
|
<button onClick={() => onDelete(row)} className="inline-flex size-7 items-center justify-center rounded-md text-destructive hover:bg-destructive/10">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
)}
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Form fields definition (for agent use — renders a form preview) */}
|
|
<div className="hidden" data-slot="crud-form-schema" data-fields={JSON.stringify(fields)} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export type { CrudPageProps, CrudField }
|