data = await FileAttachment("_data/publications.json").json()
pubs = data.publications
viewof search = Inputs.text({
label: "Search",
placeholder: "Title, author, journal...",
width: "100%"
})
viewof yearFilter = Inputs.select(
["All years", ...new Set(pubs.map(p => p.year).filter(Boolean).sort().reverse())],
{ label: "Year" }
)
viewof perOnly = Inputs.toggle({ label: "Show all works (including non-PER)", value: false })
filtered = pubs.filter(p => {
const q = search.toLowerCase()
const matchSearch = !q ||
(p.title || "").toLowerCase().includes(q) ||
new RegExp("\\b" + q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b", "i").test(p.authors_full || "") ||
(p.journal || "").toLowerCase().includes(q)
const matchYear = yearFilter === "All years" || p.year === yearFilter
const matchPer = perOnly || p.is_per
return matchSearch && matchYear && matchPer
})
md`**Showing ${filtered.length} of ${pubs.length} publications** — last updated ${data.last_updated}`htl.html`<table style="width:100%; border-collapse:collapse; font-size:0.9rem;">
<thead>
<tr style="border-bottom:2px solid #ccc; text-align:left;">
<th style="padding:8px 12px; width:55px;">Year</th>
<th style="padding:8px 12px;">Title</th>
<th style="padding:8px 12px; width:200px;">Authors</th>
<th style="padding:8px 12px; width:220px;">Journal</th>
<th style="padding:8px 12px; width:50px;">PER</th>
</tr>
</thead>
<tbody>
${filtered.map((p, i) => htl.html`<tr style="border-bottom:1px solid #eee; background:${i % 2 === 0 ? '#fff' : '#f9f9f9'};">
<td style="padding:8px 12px; color:#666;">${p.year || "—"}</td>
<td style="padding:8px 12px;">
${p.url
? htl.html`<a href="${p.url}" target="_blank" style="color:#1a6496;">${p.title}</a>`
: p.title}
</td>
<td style="padding:8px 12px; color:#444;">${p.authors_display || ""}</td>
<td style="padding:8px 12px; color:#444; font-style:italic;">${p.journal || ""}</td>
<td style="padding:8px 12px; text-align:center;">${p.is_per ? "✓" : ""}</td>
</tr>`)}
</tbody>
</table>`