@opennodes/registry 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/web/index.html +46 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennodes/registry",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "OpenNodes (ONP) reference registry: node registration with proof-of-resource qualification (Stage A/B/C), transparent ranking, enforceable estimates, catalog importers (OpenRouter, Hugging Face, models.dev, Ollama), MCP surface, and the web app (catalog, chat, advisor, Node Builder)",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -47,6 +47,12 @@
47
47
  .mitem .t .nm { font-weight: 600; font-size: 0.86rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
48
48
  .mitem .b { display: flex; gap: 0.8rem; margin-top: 0.25rem; font-size: 0.7rem; color: var(--muted); font-variant-numeric: tabular-nums; flex-wrap: wrap; }
49
49
  .mdetail { min-height: 30rem; }
50
+ .pager { display: flex; align-items: center; justify-content: center; gap: 0.3rem; margin-top: 0.5rem; font-size: 0.78rem; color: var(--muted); flex-wrap: wrap; }
51
+ .pager:empty { display: none; }
52
+ .pgbtn { font: inherit; min-width: 2rem; padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--line); background: var(--panel); color: var(--accent); cursor: pointer; }
53
+ .pgbtn:disabled { color: var(--faint); cursor: default; }
54
+ .pgnum { padding: 0 0.3rem; }
55
+ .pgnum input { width: 4rem; font: inherit; padding: 0.2rem 0.3rem; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); color: var(--ink); text-align: center; }
50
56
 
51
57
  /* chat */
52
58
  .chatgrid { display: grid; grid-template-columns: minmax(0, 1fr) 19rem; gap: 1.2rem; align-items: start; }
@@ -244,7 +250,7 @@
244
250
  <div id="adv-out"></div>
245
251
  </details>
246
252
  <div class="mgrid">
247
- <div class="mlist" id="m-list"></div>
253
+ <div class="mcol"><div class="mlist" id="m-list"></div><div class="pager" id="m-pager"></div></div>
248
254
  <div class="mdetail" id="m-detail"><p class="sub">Select a model from the list.</p></div>
249
255
  </div>
250
256
  </div>
@@ -607,34 +613,47 @@ function provLine(o) {
607
613
  }
608
614
 
609
615
  const PAGE = 60;
610
- let listQuery = '', listTotal = 0;
611
-
612
- // Count line and the "Show more" button at the end of the list.
613
- function updateListFooter() {
614
- $('#m-more')?.remove();
615
- const shown = lastResults.length;
616
- $('#m-counts').textContent = fmtInt(listTotal) + ' offerings' + (shown < listTotal ? ` · showing 1–${fmtInt(shown)}` : '');
617
- if (shown < listTotal) {
618
- $('#m-list').insertAdjacentHTML('beforeend', `<button class="btn" id="m-more" style="background:var(--panel); color:var(--accent); align-self:stretch">Show ${fmtInt(Math.min(PAGE, listTotal - shown))} more</button>`);
619
- $('#m-more').addEventListener('click', loadMore);
620
- }
616
+ let listQuery = '', listTotal = 0, listPage = 0;
617
+
618
+ // Count line and page controls under the list: first / previous / page N of M / next / last.
619
+ function updatePager() {
620
+ const pages = Math.max(1, Math.ceil(listTotal / PAGE));
621
+ const from = listTotal ? listPage * PAGE + 1 : 0, to = Math.min(listTotal, (listPage + 1) * PAGE);
622
+ $('#m-counts').textContent = fmtInt(listTotal) + ' offerings' + (pages > 1 ? ` · ${fmtInt(from)}–${fmtInt(to)}` : '');
623
+ const pager = $('#m-pager');
624
+ if (pages <= 1) { pager.innerHTML = ''; return; }
625
+ const btn = (label, page, title) => `<button class="pgbtn" data-page="${page}" title="${title}"${page < 0 || page >= pages || page === listPage ? ' disabled' : ''}>${label}</button>`;
626
+ pager.innerHTML = btn('«', 0, 'first page') + btn('‹', listPage - 1, 'previous page')
627
+ + `<span class="pgnum">Page <input id="m-page" type="number" min="1" max="${pages}" value="${listPage + 1}" aria-label="page number"> of ${fmtInt(pages)}</span>`
628
+ + btn('›', listPage + 1, 'next page') + btn('»', pages - 1, 'last page');
629
+ pager.querySelectorAll('.pgbtn').forEach((b) => b.addEventListener('click', () => goToPage(Number(b.dataset.page))));
630
+ $('#m-page').addEventListener('change', (e) => goToPage(Math.min(pages, Math.max(1, Math.round(Number(e.target.value) || 1))) - 1));
621
631
  }
622
632
 
623
- async function loadMore() {
624
- const btn = $('#m-more');
625
- btn.disabled = true;
633
+ async function goToPage(page) {
626
634
  const query = listQuery;
627
635
  const p = new URLSearchParams(query);
628
- p.set('offset', lastResults.length);
636
+ p.set('offset', page * PAGE);
637
+ $('#m-pager').querySelectorAll('button, input').forEach((el) => { el.disabled = true; });
629
638
  try {
630
- const { offerings } = await api('/v0/offerings?' + p);
639
+ const { offerings, total } = await api('/v0/offerings?' + p);
631
640
  if (query !== listQuery) return; // the filters changed meanwhile
632
- const start = lastResults.length;
633
- lastResults = lastResults.concat(offerings);
634
- btn.insertAdjacentHTML('beforebegin', offerings.map(renderItem).join(''));
635
- [...document.querySelectorAll('#m-list .mitem')].slice(start).forEach((el) => el.addEventListener('click', () => selectModel(el)));
636
- updateListFooter();
637
- } catch (e) { btn.disabled = false; btn.textContent = 'Retry — ' + e.message; }
641
+ listPage = page;
642
+ listTotal = total ?? listTotal;
643
+ lastResults = offerings;
644
+ renderList(offerings);
645
+ $('#m-list').scrollTop = 0;
646
+ updatePager();
647
+ } catch (e) { updatePager(); $('#m-pager').insertAdjacentHTML('beforeend', '<span class="err">' + esc(e.message) + '</span>'); }
648
+ }
649
+
650
+ // Render one page of the list, keeping the highlighted model when it is on this page.
651
+ function renderList(offerings) {
652
+ $('#m-list').innerHTML = offerings.map(renderItem).join('')
653
+ || '<p class="sub">No offerings match. A fresh registry fills up via catalog imports and node registrations.</p>';
654
+ document.querySelectorAll('.mitem').forEach((el) => el.addEventListener('click', () => selectModel(el)));
655
+ const current = $('#model-card')?.dataset.artifact;
656
+ document.querySelectorAll('.mitem').forEach((el) => el.classList.toggle('sel', el.dataset.artifact === current));
638
657
  }
639
658
 
640
659
  async function search() {
@@ -652,17 +671,16 @@ async function search() {
652
671
  if (listQuery !== p.toString()) return; // a newer search is under way
653
672
  lastResults = offerings;
654
673
  listTotal = total ?? offerings.length;
655
- $('#m-list').innerHTML = offerings.map(renderItem).join('')
656
- || '<p class="sub">No offerings match. A fresh registry fills up via catalog imports and node registrations.</p>';
657
- updateListFooter();
658
- document.querySelectorAll('.mitem').forEach((el) => el.addEventListener('click', () => selectModel(el)));
674
+ listPage = 0;
675
+ renderList(offerings);
676
+ updatePager();
659
677
  // keep the current selection when still listed, else select the first result
660
678
  const current = $('#model-card')?.dataset.artifact;
661
679
  const keep = current && [...document.querySelectorAll('.mitem')].find((el) => el.dataset.artifact === current);
662
680
  const target = keep ?? document.querySelector('.mitem');
663
681
  if (target) selectModel(target);
664
682
  else $('#m-detail').innerHTML = '<p class="sub">No offerings match.</p>';
665
- } catch (e) { $('#m-list').innerHTML = '<p class="err">' + esc(e.message) + '</p>'; }
683
+ } catch (e) { $('#m-list').innerHTML = '<p class="err">' + esc(e.message) + '</p>'; $('#m-pager').innerHTML = ''; }
666
684
  }
667
685
 
668
686
  /* ---------- advisor (ONP-3 §5b) ---------- */