diff --git a/frontend/app.js b/frontend/app.js index 4f4b54b..cdc6517 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -4,6 +4,8 @@ const SCHEMA_URL = "/schemeta.schema.json"; const NET_CLASSES = ["power", "ground", "signal", "analog", "differential", "clock", "bus"]; const PIN_SIDES = ["left", "right", "top", "bottom"]; const PIN_TYPES = ["power_in", "power_out", "input", "output", "bidirectional", "passive", "analog", "ground"]; +const LIST_ROW_HEIGHT = 36; +const LIST_OVERSCAN_ROWS = 8; const state = { model: null, @@ -721,6 +723,26 @@ function resolveLabelCollisions(svg) { } } +function renderVirtualList(listEl, allItems, renderRow, rowHeight = LIST_ROW_HEIGHT) { + const items = Array.isArray(allItems) ? allItems : []; + if (!items.length) { + listEl.innerHTML = ""; + return; + } + + const viewportHeight = Math.max(rowHeight * 4, listEl.clientHeight || 230); + const scrollTop = listEl.scrollTop; + const total = items.length; + const visibleRows = Math.ceil(viewportHeight / rowHeight); + const start = Math.max(0, Math.floor(scrollTop / rowHeight) - LIST_OVERSCAN_ROWS); + const end = Math.min(total, start + visibleRows + LIST_OVERSCAN_ROWS * 2); + const topPad = start * rowHeight; + const bottomPad = (total - end) * rowHeight; + + const rows = items.slice(start, end).map(renderRow).join(""); + listEl.innerHTML = `
  • ${rows}
  • `; +} + function renderInstances() { if (!state.model) { el.instanceList.innerHTML = ""; @@ -730,12 +752,15 @@ function renderInstances() { const q = el.instanceFilter.value.trim().toLowerCase(); const items = state.model.instances.filter((i) => i.ref.toLowerCase().includes(q)); - el.instanceList.innerHTML = items - .map((inst) => { + renderVirtualList( + el.instanceList, + items, + (inst) => { const cls = state.selectedRefs.includes(inst.ref) ? "active" : ""; return `
  • ${inst.ref} ยท ${inst.symbol}
  • `; - }) - .join(""); + }, + LIST_ROW_HEIGHT + ); } function renderNets() { @@ -747,12 +772,15 @@ function renderNets() { const q = el.netFilter.value.trim().toLowerCase(); const items = state.model.nets.filter((n) => n.name.toLowerCase().includes(q)); - el.netList.innerHTML = items - .map((net) => { + renderVirtualList( + el.netList, + items, + (net) => { const cls = net.name === state.selectedNet ? "active" : ""; return `
  • ${net.name} (${net.class})
  • `; - }) - .join(""); + }, + LIST_ROW_HEIGHT + ); } function netByName(name) { @@ -1843,8 +1871,16 @@ async function loadSample() { } function setupEvents() { - el.instanceFilter.addEventListener("input", renderInstances); - el.netFilter.addEventListener("input", renderNets); + el.instanceFilter.addEventListener("input", () => { + el.instanceList.scrollTop = 0; + renderInstances(); + }); + el.netFilter.addEventListener("input", () => { + el.netList.scrollTop = 0; + renderNets(); + }); + el.instanceList.addEventListener("scroll", renderInstances, { passive: true }); + el.netList.addEventListener("scroll", renderNets, { passive: true }); el.instanceList.addEventListener("click", (evt) => { const item = evt.target.closest("[data-ref-item]"); diff --git a/frontend/styles.css b/frontend/styles.css index a451a19..ba84fe5 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -182,6 +182,13 @@ textarea { background: var(--accent-soft); } +.list li.listSpacer { + padding: 0; + border: none; + cursor: default; + background: transparent; +} + .card { border: 1px solid var(--line); border-radius: 8px;