diff --git a/scripts/check.mjs b/scripts/check.mjs index f5a2424..baa0220 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -108,6 +108,7 @@ for (const value of [ 'id="library"', 'id="submit"', 'id="loop-search"', + 'id="loop-sort"', 'id="library-pagination"', 'name="loop-library-form-api"', "https://signals.forwardfuture.ai/loop-library/catalog.json", @@ -121,7 +122,10 @@ assert.equal((html.match(/data-here-now-credit/g) || []).length, 2); assert(learnHtml.includes("How agent loops work")); assert(agentHtml.includes("For AI agents")); assert(css.includes(".loop-row")); +assert(css.includes(".sort-control")); assert(browserScript.includes("data-category-filter")); +assert(browserScript.includes('sortSelect.addEventListener("change"')); +assert(browserScript.includes('params.set("sort", activeSort)')); assert(browserScript.includes("library-pagination")); assert(!browserScript.includes("innerHTML")); diff --git a/site/index.html b/site/index.html index 5dd99c5..399c617 100644 --- a/site/index.html +++ b/site/index.html @@ -132,7 +132,7 @@ href="https://signals.forwardfuture.ai/loop-library/agents/" /> - + - + Loop Library: Repeatable AI Agent Workflows | Forward Future @@ -367,59 +367,70 @@

/> -
- - - - - - + + + + + + +
+ + diff --git a/site/script.js b/site/script.js index 0b98d35..2846426 100644 --- a/site/script.js +++ b/site/script.js @@ -59,6 +59,7 @@ const loopRows = [...document.querySelectorAll(".loop-row")]; const categoryFilters = [ ...document.querySelectorAll("[data-category-filter]"), ]; +const sortSelect = document.querySelector("#loop-sort"); const resultsCount = document.querySelector("#results-count"); const emptyState = document.querySelector("#empty-state"); const pagination = document.querySelector("#library-pagination"); @@ -72,7 +73,27 @@ const loopTableBody = document.querySelector(".loop-table tbody"); const loopRowPositions = new Map( loopRows.map((row, index) => [row, index]), ); -loopRows.sort((a, b) => { +const SORT_OPTIONS = new Set(["featured", "newest", "alphabetical"]); + +let activeSort = "featured"; + +function rowTitle(row) { + return normalize(row.querySelector(".loop-title-link")?.textContent ?? ""); +} + +function compareNewest(a, b) { + const publishedDifference = (b.dataset.published ?? "").localeCompare( + a.dataset.published ?? "", + ); + + if (publishedDifference !== 0) { + return publishedDifference; + } + + return loopRowPositions.get(b) - loopRowPositions.get(a); +} + +function compareFeatured(a, b) { const featuredDifference = Number(b.dataset.featured === "true") - Number(a.dataset.featured === "true"); @@ -82,24 +103,36 @@ loopRows.sort((a, b) => { } if (a.dataset.featured === "true") { - return 0; + return loopRowPositions.get(a) - loopRowPositions.get(b); } - const publishedDifference = b.dataset.published.localeCompare( - a.dataset.published, - ); + return compareNewest(a, b); +} - if (publishedDifference !== 0) { - return publishedDifference; +function applySort(sort) { + activeSort = SORT_OPTIONS.has(sort) ? sort : "featured"; + + if (sortSelect) { + sortSelect.value = activeSort; } - return loopRowPositions.get(b) - loopRowPositions.get(a); -}); + loopRows.sort((a, b) => { + if (activeSort === "alphabetical") { + return rowTitle(a).localeCompare(rowTitle(b), undefined, { + sensitivity: "base", + }); + } -if (loopTableBody) { - loopRows.forEach((row) => loopTableBody.append(row)); + return activeSort === "newest" ? compareNewest(a, b) : compareFeatured(a, b); + }); + + if (loopTableBody) { + loopRows.forEach((row) => loopTableBody.append(row)); + } } +applySort(activeSort); + // Snapshot each row's searchable text before the prompt toggle is injected // below. Read the loop content rather than the whole row so search does not // match interface controls such as "Copy loop" or "Show more". @@ -250,6 +283,12 @@ function syncUrlState(method = "replace") { params.delete("category"); } + if (activeSort !== "featured") { + params.set("sort", activeSort); + } else { + params.delete("sort"); + } + if (currentPage > 1) { params.set("page", String(currentPage)); } else { @@ -281,6 +320,7 @@ function readUrlState() { } applyCategory(params.get("category") ?? "all"); + applySort(params.get("sort") ?? "featured"); const requestedPage = Number.parseInt(params.get("page") ?? "1", 10); currentPage = @@ -337,6 +377,15 @@ categoryFilters.forEach((filter) => { }); }); +if (sortSelect) { + sortSelect.addEventListener("change", () => { + applySort(sortSelect.value); + currentPage = 1; + updateLibrary(); + syncUrlState("push"); + }); +} + const clearFiltersButton = document.querySelector("#clear-filters"); if (clearFiltersButton) { diff --git a/site/styles.css b/site/styles.css index e790700..2c98ea2 100644 --- a/site/styles.css +++ b/site/styles.css @@ -652,6 +652,13 @@ code { gap: 8px; } +.library-refinements { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + .category-filter { min-height: 32px; padding: 6px 10px; @@ -678,6 +685,37 @@ code { outline-offset: 2px; } +.sort-control { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 8px; + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.62rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.sort-control select { + min-height: 32px; + padding: 6px 8px; + border: 1px solid var(--ink); + border-radius: 0; + color: var(--ink); + background: var(--surface); + cursor: pointer; + font: inherit; + letter-spacing: inherit; + text-transform: uppercase; +} + +.sort-control select:focus-visible { + outline: 2px solid var(--orange); + outline-offset: 2px; +} + .copy-button { border: 1px solid var(--ink); color: var(--ink); @@ -1932,6 +1970,11 @@ code { width: 100%; } + .library-refinements { + align-items: flex-start; + flex-direction: column; + } + .results-line span, .results-line time { display: none;