/** * Preact Component Library (No Build) * Reusable components for DocuPDF templates * Import via ES modules from templates */ const { h, Fragment } = window.PreactLib || {}; const { useState, useEffect, useCallback, useMemo } = window.PreactLib || {}; // === Button Component === export function Button({ children, variant = 'primary', size = '', pill = false, disabled = false, loading = false, icon = null, onClick, className = '', ...props }) { const html = window.html; const variantClasses = { primary: 'btn-primary', secondary: 'btn-secondary', success: 'btn-success', danger: 'btn-danger', warning: 'btn-warning', info: 'btn-info', light: 'btn-light', dark: 'btn-dark', outline: 'btn-outline-primary', outlineSuccess: 'btn-outline-success', outlineDanger: 'btn-outline-danger' }; const sizeClasses = { sm: 'btn-sm', lg: 'btn-lg' }; const classes = [ 'btn', variantClasses[variant] || variantClasses.primary, sizeClasses[size] || '', pill ? 'btn-pill' : '', className ].filter(Boolean).join(' '); return html` `; } // === Card Component === export function Card({ children, title = null, header = null, footer = null, className = '', hoverable = false, ...props }) { const html = window.html; const classes = [ 'card', 'bg-dark', 'text-white', hoverable ? 'card-hover' : '', className ].filter(Boolean).join(' '); return html`
${(title || header) ? html`
${header || html`
${title}
`}
` : ''}
${children}
${footer ? html`` : ''}
`; } // === Badge Component === export function Badge({ children, variant = 'secondary', className = '', ...props }) { const html = window.html; const variantClasses = { primary: 'bg-primary', secondary: 'bg-secondary', success: 'bg-success', danger: 'bg-danger', warning: 'bg-warning text-dark', info: 'bg-info text-dark', light: 'bg-light text-dark', dark: 'bg-dark' }; const classes = [ 'badge', variantClasses[variant] || variantClasses.secondary, className ].filter(Boolean).join(' '); return html` ${children} `; } // === Alert Component === export function Alert({ children, variant = 'info', dismissible = false, onDismiss, className = '', ...props }) { const html = window.html; const [visible, setVisible] = useState(true); const variantClasses = { primary: 'alert-primary', secondary: 'alert-secondary', success: 'alert-success', danger: 'alert-danger', warning: 'alert-warning', info: 'alert-info', light: 'alert-light', dark: 'alert-dark' }; const classes = [ 'alert', variantClasses[variant] || variantClasses.info, dismissible ? 'alert-dismissible fade show' : '', className ].filter(Boolean).join(' '); if (!visible) return null; return html` `; } // === Form Input Component === export function FormInput({ label = null, type = 'text', value = '', onInput, error = null, helpText = null, className = '', ...props }) { const html = window.html; return html`
${label ? html`` : ''} ${error ? html`
${error}
` : ''} ${helpText && !error ? html`
${helpText}
` : ''}
`; } // === Form Select Component === export function FormSelect({ label = null, value = '', onChange, options = [], placeholder = 'Select...', error = null, className = '', ...props }) { const html = window.html; return html`
${label ? html`` : ''} ${error ? html`
${error}
` : ''}
`; } // === Modal Component === export function Modal({ show = false, title = '', children, footer = null, onClose, size = 'md', staticBackdrop = false, }) { const html = window.html; const modalRef = useRef(null); useEffect(() => { if (show && modalRef.current) { const bsModal = new bootstrap.Modal(modalRef.current, { backdrop: staticBackdrop ? 'static' : true, keyboard: !staticBackdrop }); bsModal.show(); const handleHidden = () => { if (onClose) onClose(); }; modalRef.current.addEventListener('hidden.bs.modal', handleHidden); return () => { modalRef.current.removeEventListener('hidden.bs.modal', handleHidden); bsModal.dispose(); }; } }, [show]); const sizeClasses = { sm: 'modal-sm', md: '', lg: 'modal-lg', xl: 'modal-xl' }; return html` `; } // === Spinner Component === export function Spinner({ variant = 'primary', size = 'md', className = '' }) { const html = window.html; const sizeClasses = { sm: 'spinner-border-sm', md: 'spinner-border', lg: 'spinner-grow-lg' }; return html`
Loading...
`; } // === Table Components === export function Table({ columns = [], data = [], keyField = 'id', onRowClick = null, className = '', striped = true, hover = true, renderCell = null, emptyMessage = 'No data available' }) { const html = window.html; return html`
${columns.map(col => html` `)} ${data.length === 0 ? html` ` : data.map(row => html` onRowClick(row)}` : ''} style="${onRowClick ? 'cursor: pointer;' : ''}" > ${columns.map(col => html` `)} `)}
${col.header}
${emptyMessage}
${renderCell ? renderCell(row, col.key) : row[col.key] }
`; } // === Progress Bar Component === export function ProgressBar({ value = 0, max = 100, variant = 'primary', label = null, striped = false, animated = false, className = '' }) { const html = window.html; const percentage = Math.min(100, Math.max(0, (value / max) * 100)); const variantClasses = { primary: 'bg-primary', success: 'bg-success', danger: 'bg-danger', warning: 'bg-warning', info: 'bg-info' }; return html`
${label !== null ? label : `${Math.round(percentage)}%`}
`; } // === Tabs Component === export function Tabs({ tabs = [], activeTab = 0, onChange, className = '' }) { const html = window.html; const [active, setActive] = useState(activeTab); const handleChange = (index) => { setActive(index); if (onChange) onChange(index, tabs[index]); }; return html` ${tabs[active]?.content} `; } // === Checkbox Component === export function Checkbox({ label = null, checked = false, onChange, disabled = false, className = '' }) { const html = window.html; return html`
${label ? html` ` : ''}
`; } // === Toggle Switch Component === export function Toggle({ label = null, checked = false, onChange, disabled = false, className = '' }) { const html = window.html; return html`
${label ? html`` : ''}
`; } // === File Upload Component === export function FileUpload({ accept = '*', multiple = false, onFilesSelected, children = null, className = '' }) { const html = window.html; const inputRef = useRef(null); const handleChange = (e) => { const files = Array.from(e.target.files); if (onFilesSelected) onFilesSelected(files); // Reset input so same file can be selected again e.target.value = ''; }; const handleClick = () => { inputRef.current?.click(); }; return html` <> ${children ? children({ onClick: handleClick }) : html`<${Button} onClick=${handleClick}>Choose Files` } `; } // === Search Input Component === export function SearchInput({ value = '', onSearch, placeholder = 'Search...', debounceMs = 300, className = '' }) { const html = window.html; const [localValue, setLocalValue] = useState(value); useEffect(() => { const timer = setTimeout(() => { if (onSearch) onSearch(localValue); }, debounceMs); return () => clearTimeout(timer); }, [localValue]); return html`
setLocalValue(e.target.value)} />
`; } // Export all components export const Components = { Button, Card, Badge, Alert, FormInput, FormSelect, Modal, Spinner, Table, ProgressBar, Tabs, Checkbox, Toggle, FileUpload, SearchInput };