ifrs9-ecl-copilot / app /ui /src /components /SearchableTable.jsx
Preetomsorkar's picture
UI v3: fintech-direction design pass (5 grafts), waterfall historical-mode fix, explain-strip placement + shared hook, palette contrast fix
20abf65 verified
Raw
History Blame Contribute Delete
2.02 kB
import { useMemo, useState } from 'preact/hooks';
/** Generic searchable table. `columns`: [{key, label, render?}]. `rows`: array
* of plain objects. Search matches any column's stringified value. Renders
* markdown-lite backtick/glyph cells as-is (the API sends raw source cells). */
export default function SearchableTable({ columns, rows, placeholder, caption }) {
const [q, setQ] = useState('');
const filtered = useMemo(() => {
const needle = q.trim().toLowerCase();
if (!needle) return rows;
return rows.filter((r) =>
columns.some((c) => String(r[c.key] ?? '').toLowerCase().includes(needle)),
);
}, [q, rows, columns]);
return (
<div class="searchable-table">
<div class="table-toolbar">
<input
type="search"
class="table-search"
placeholder={placeholder || 'Search…'}
value={q}
onInput={(e) => setQ(e.currentTarget.value)}
/>
<span class="table-count">
{filtered.length} / {rows.length} rows
</span>
</div>
{caption && <p class="panel-sub">{caption}</p>}
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
{columns.map((c) => (
<th class={c.align === 'right' ? 'num' : ''} key={c.key}>{c.label}</th>
))}
</tr>
</thead>
<tbody>
{filtered.length === 0 && (
<tr>
<td class="empty-note" colSpan={columns.length}>
No rows match “{q}”.
</td>
</tr>
)}
{filtered.map((r, i) => (
<tr key={i}>
{columns.map((c) => (
<td class={c.align === 'right' ? 'num' : ''} key={c.key}>
{c.render ? c.render(r) : r[c.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}