// Piezas visuales del DocumentEditor: secciones, sección plegable, tarjeta de // contraparte, totales, revisión y vista previa. Sin estado de negocio: el // editor (o un remote federado) les pasa los datos ya calculados. Los contratos // de datos viven en primitives/panels.ts. import { useId, useState, type ReactNode } from 'react' import { Badge } from '@asteby/metacore-ui/primitives' import { AlertTriangle, ChevronDown, XCircle } from 'lucide-react' import type { CreditStatus, EditorIssue } from './document-editor-model' import type { Formatter } from './format' import { computeLine, type LineItem } from './line-items' /** Bloque con título discreto y espaciado generoso (sin bordes pesados). */ export function EditorSection({ title, hint, children, slot }: { title?: ReactNode; hint?: ReactNode; children: ReactNode; slot?: string }) { return (
{(title || hint) && (
{title &&

{title}

} {hint && {hint}}
)} {children}
) } export interface CollapsibleSectionProps { title: ReactNode /** Resumen de una línea cuando está cerrada (p. ej. «G03 · PUE»). */ summary?: ReactNode open?: boolean defaultOpen?: boolean onOpenChange?: (open: boolean) => void children: ReactNode slot?: string } /** * Sección plegable. El contenido queda MONTADO aunque esté cerrada (`hidden`): * las contribuciones de otros addons siguen aplicando defaults y reportando * revisiones sin que el usuario abra la sección. */ export function CollapsibleSection({ title, summary, open, defaultOpen = false, onOpenChange, children, slot }: CollapsibleSectionProps) { const [inner, setInner] = useState(defaultOpen) const isOpen = open ?? inner const id = useId() const toggle = () => { setInner(!isOpen) onOpenChange?.(!isOpen) } return (
) } export interface PartyCardProps { name?: string rows: Array<{ key: string; label: string; value: string }> credit?: CreditStatus | null fmt: Formatter children?: ReactNode } /** Tarjeta compacta de la contraparte: nombre, 3-6 datos clave y estado de crédito. */ export function PartyCard({ name, rows, credit, fmt, children }: PartyCardProps) { return (
{name && {name}} {credit && ( {credit.holdReason ? credit.holdReason : credit.available != null ? `Crédito disponible ${fmt.money(credit.available)}` : `Saldo ${fmt.money(credit.balance ?? 0)}`} )}
{rows.length > 0 && (
{rows.map((r) => (
{r.label}
{r.value}
))}
)} {children}
) } export interface TotalsRow { label: string value: string emphasis?: boolean } /** Totales alineados a la derecha; la última fila con `emphasis` es el total. */ export function TotalsPanel({ rows, muted = false }: { rows: TotalsRow[]; muted?: boolean }) { return (
{rows.map((r, i) => (
0 ? 'border-t pt-2.5' : ''} text-lg font-semibold` : ''}`} >
{r.label}
{r.value}
))}
) } /** Lista de revisión: errores (rojo) y avisos (ámbar). Vacía → no pinta nada. */ export function ValidationChecklist({ issues, title = 'Revisa antes de guardar' }: { issues: EditorIssue[]; title?: string }) { if (issues.length === 0) return null return (

{title}

) } export interface PreviewContent { xml?: string html?: string pdf_url?: string pdf_base64?: string } /** Contenido de la vista previa (PDF, HTML o XML, en ese orden de preferencia). */ export function PreviewPanel({ preview }: { preview: PreviewContent | null }) { if (!preview) return null const pdf = preview.pdf_url ?? (preview.pdf_base64 ? `data:application/pdf;base64,${preview.pdf_base64}` : undefined) if (pdf) return