diff --git a/apps/api/src/modules/listings/repositories/listing-repository.js b/apps/api/src/modules/listings/repositories/listing-repository.js
index a10fa22..c0c3364 100644
--- a/apps/api/src/modules/listings/repositories/listing-repository.js
+++ b/apps/api/src/modules/listings/repositories/listing-repository.js
@@ -135,7 +135,13 @@ export function createListingRepository(db) {
const offset = (page - 1) * limit;
const sortOrder =
- sort === "price_low"
+ sort === "updated"
+ ? desc(listings.updatedAt)
+ : sort === "name_asc"
+ ? products.name
+ : sort === "name_desc"
+ ? desc(products.name)
+ : sort === "price_low"
? listings.askingPriceMinor
: sort === "price_high"
? desc(listings.askingPriceMinor)
diff --git a/apps/api/src/modules/search/domain/search-query.js b/apps/api/src/modules/search/domain/search-query.js
index b224776..b37e7e5 100644
--- a/apps/api/src/modules/search/domain/search-query.js
+++ b/apps/api/src/modules/search/domain/search-query.js
@@ -58,8 +58,8 @@ export function createSearchQuery({
throw new Error("minPriceMinor must be <= maxPriceMinor");
}
- if (!["recent", "price_low", "price_high"].includes(sort)) {
- throw new Error("sort must be recent, price_low, or price_high");
+ if (!["recent", "updated", "name_asc", "name_desc", "price_low", "price_high"].includes(sort)) {
+ throw new Error("sort must be recent, updated, name_asc, name_desc, price_low, or price_high");
}
return {
diff --git a/apps/api/tests/domain/search-query.test.js b/apps/api/tests/domain/search-query.test.js
index 54cef33..6c57159 100644
--- a/apps/api/tests/domain/search-query.test.js
+++ b/apps/api/tests/domain/search-query.test.js
@@ -57,6 +57,9 @@ test("builds a public query filter set that hides sold items by default", () =>
test("accepts only the supported public search sort values", () => {
assert.equal(createSearchQuery({ sort: "price_low" }).sort, "price_low");
+ assert.equal(createSearchQuery({ sort: "updated" }).sort, "updated");
+ assert.equal(createSearchQuery({ sort: "name_asc" }).sort, "name_asc");
+ assert.equal(createSearchQuery({ sort: "name_desc" }).sort, "name_desc");
assert.throws(
() => createSearchQuery({ sort: "random" }),
/sort must be recent/,
diff --git a/apps/web/src/components/atoms/CatalogSortSelect.scss b/apps/web/src/components/atoms/CatalogSortSelect.scss
new file mode 100644
index 0000000..699242b
--- /dev/null
+++ b/apps/web/src/components/atoms/CatalogSortSelect.scss
@@ -0,0 +1,18 @@
+.catalog-sort-select {
+ display: grid;
+ gap: var(--space-1);
+}
+
+.catalog-sort-select span {
+ color: var(--color-text-muted);
+ font-size: var(--text-sm);
+}
+
+.catalog-sort-select select {
+ min-height: 44px;
+ border: var(--border-thin) solid var(--color-border);
+ border-radius: var(--radius-md);
+ padding: var(--space-2) var(--space-3);
+ background: var(--color-surface);
+ color: var(--color-text);
+}
diff --git a/apps/web/src/components/atoms/CatalogSortSelect.stories.js b/apps/web/src/components/atoms/CatalogSortSelect.stories.js
new file mode 100644
index 0000000..3569430
--- /dev/null
+++ b/apps/web/src/components/atoms/CatalogSortSelect.stories.js
@@ -0,0 +1,6 @@
+import CatalogSortSelect from './CatalogSortSelect.vue';
+
+export default { title: 'Atoms/CatalogSortSelect', component: CatalogSortSelect };
+
+export const RecentlyAdded = { args: { modelValue: 'recent' } };
+export const NameAscending = { args: { modelValue: 'name_asc' } };
diff --git a/apps/web/src/components/atoms/CatalogSortSelect.vue b/apps/web/src/components/atoms/CatalogSortSelect.vue
new file mode 100644
index 0000000..abe5fb6
--- /dev/null
+++ b/apps/web/src/components/atoms/CatalogSortSelect.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/apps/web/src/pages/CatalogPage.vue b/apps/web/src/pages/CatalogPage.vue
index 69688db..dc309e4 100644
--- a/apps/web/src/pages/CatalogPage.vue
+++ b/apps/web/src/pages/CatalogPage.vue
@@ -8,6 +8,7 @@ import {
listPublicConditions,
} from "@/services/catalog-service.js";
import ListingCard from "@/components/molecules/ListingCard.vue";
+import CatalogSortSelect from "@/components/atoms/CatalogSortSelect.vue";
const route = useRoute();
const router = useRouter();
@@ -124,14 +125,7 @@ watch(
>Max price (cents)
-
+
diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md
index f5140da..e7c3939 100644
--- a/docs/IMPLEMENTATION-STATUS.md
+++ b/docs/IMPLEMENTATION-STATUS.md
@@ -163,6 +163,7 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro
- `npm audit --omit=dev` currently reports zero production vulnerabilities. Re-run the audit after dependency changes and review development-dependency findings separately before production releases.
- Authenticated E2E coverage now verifies both the core catalog workflow and the Settings, Saved Views, and export-controls workflow.
- Production dependency audit currently reports zero vulnerabilities with `npm audit --omit=dev`.
+- Public catalog sorting now supports recently added, recently updated, name A-Z, name Z-A, price low-to-high, and price high-to-low through a URL-synchronized atomic sort control. Broader inventory filter parity and market-value sorting remain.
## Local Commands