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;