root
Working CHanges to revesion ; add preact support
e8a57cb
/**
* 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`
<button
class=${classes}
disabled=${disabled || loading}
onClick=${onClick}
...=${props}
>
${loading ? html`<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>` : ''}
${icon ? html`<i class="${icon} me-2"></i>` : ''}
${children}
</button>
`;
}
// === 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`
<div class=${classes} ...=${props}>
${(title || header) ? html`
<div class="card-header">
${header || html`<h5 class="mb-0">${title}</h5>`}
</div>
` : ''}
<div class="card-body">
${children}
</div>
${footer ? html`<div class="card-footer">${footer}</div>` : ''}
</div>
`;
}
// === 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`
<span class=${classes} ...=${props}>${children}</span>
`;
}
// === 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`
<div class=${classes} role="alert" ...=${props}>
${children}
${dismissible ? html`
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
onClick=${() => {
setVisible(false);
if (onDismiss) onDismiss();
}}
></button>
` : ''}
</div>
`;
}
// === Form Input Component ===
export function FormInput({
label = null,
type = 'text',
value = '',
onInput,
error = null,
helpText = null,
className = '',
...props
}) {
const html = window.html;
return html`
<div class="mb-3 ${className}">
${label ? html`<label class="form-label">${label}</label>` : ''}
<input
type=${type}
class="form-control ${error ? 'is-invalid' : ''}"
value=${value}
onInput=${onInput}
...=${props}
/>
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
${helpText && !error ? html`<div class="form-text">${helpText}</div>` : ''}
</div>
`;
}
// === Form Select Component ===
export function FormSelect({
label = null,
value = '',
onChange,
options = [],
placeholder = 'Select...',
error = null,
className = '',
...props
}) {
const html = window.html;
return html`
<div class="mb-3 ${className}">
${label ? html`<label class="form-label">${label}</label>` : ''}
<select
class="form-select ${error ? 'is-invalid' : ''}"
value=${value}
onChange=${onChange}
...=${props}
>
${placeholder ? html`<option value="">${placeholder}</option>` : ''}
${options.map(opt =>
typeof opt === 'string'
? html`<option value=${opt}>${opt}</option>`
: html`<option value=${opt.value}>${opt.label}</option>`
)}
</select>
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
</div>
`;
}
// === 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`
<div
class="modal fade"
ref=${modalRef}
tabIndex="-1"
aria-hidden="true"
>
<div class="modal-dialog ${sizeClasses[size] || ''}">
<div class="modal-content">
${title ? html`
<div class="modal-header">
<h5 class="modal-title">${title}</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
onClick=${onClose}
></button>
</div>
` : ''}
<div class="modal-body">
${children}
</div>
${footer ? html`
<div class="modal-footer">
${footer}
</div>
` : ''}
</div>
</div>
</div>
`;
}
// === 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`
<div
class="${sizeClasses[size] || sizeClasses.md} text-${variant} ${className}"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
`;
}
// === 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`
<div class="table-responsive ${className}">
<table class="table ${striped ? 'table-striped' : ''} ${hover ? 'table-hover' : ''}">
<thead>
<tr>
${columns.map(col => html`
<th scope="col" style="${col.style || ''}">${col.header}</th>
`)}
</tr>
</thead>
<tbody>
${data.length === 0 ? html`
<tr>
<td colSpan=${columns.length} class="text-center py-4">${emptyMessage}</td>
</tr>
` : data.map(row => html`
<tr
key=${row[keyField]}
${onRowClick ? `onClick=${() => onRowClick(row)}` : ''}
style="${onRowClick ? 'cursor: pointer;' : ''}"
>
${columns.map(col => html`
<td>
${renderCell
? renderCell(row, col.key)
: row[col.key]
}
</td>
`)}
</tr>
`)}
</tbody>
</table>
</div>
`;
}
// === 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`
<div class="progress ${className}" role="progressbar">
<div
class="progress-bar ${variantClasses[variant]} ${striped ? 'progress-bar-striped' : ''} ${animated ? 'progress-bar-animated' : ''}"
style="width: ${percentage}%"
>
${label !== null ? label : `${Math.round(percentage)}%`}
</div>
</div>
`;
}
// === 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`
<ul class="nav nav-pills ${className}">
${tabs.map((tab, index) => html`
<li class="nav-item">
<button
class="nav-link ${active === index ? 'active' : ''}"
onClick=${() => handleChange(index)}
>
${tab.icon ? html`<i class="${tab.icon} me-1"></i>` : ''}
${tab.label}
</button>
</li>
`)}
</ul>
${tabs[active]?.content}
`;
}
// === Checkbox Component ===
export function Checkbox({
label = null,
checked = false,
onChange,
disabled = false,
className = ''
}) {
const html = window.html;
return html`
<div class="form-check ${className}">
<input
class="form-check-input"
type="checkbox"
checked=${checked}
disabled=${disabled}
onChange=${onChange}
/>
${label ? html`
<label class="form-check-label" onClick=${(e) => e.target.tagName !== 'INPUT' && onChange?.()}>
${label}
</label>
` : ''}
</div>
`;
}
// === Toggle Switch Component ===
export function Toggle({
label = null,
checked = false,
onChange,
disabled = false,
className = ''
}) {
const html = window.html;
return html`
<div class="form-check form-switch ${className}">
<input
class="form-check-input"
type="checkbox"
role="switch"
checked=${checked}
disabled=${disabled}
onChange=${onChange}
/>
${label ? html`<label class="form-check-label">${label}</label>` : ''}
</div>
`;
}
// === 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`
<>
<input
ref=${inputRef}
type="file"
accept=${accept}
multiple=${multiple}
onChange=${handleChange}
style="display: none;"
/>
${children
? children({ onClick: handleClick })
: html`<${Button} onClick=${handleClick}>Choose Files</${Button}>`
}
</>
`;
}
// === 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`
<div class="input-group ${className}">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input
type="text"
class="form-control"
placeholder=${placeholder}
value=${localValue}
onInput=${(e) => setLocalValue(e.target.value)}
/>
</div>
`;
}
// Export all components
export const Components = {
Button,
Card,
Badge,
Alert,
FormInput,
FormSelect,
Modal,
Spinner,
Table,
ProgressBar,
Tabs,
Checkbox,
Toggle,
FileUpload,
SearchInput
};