diff --git a/scripts/lib/gallery.mjs b/scripts/lib/gallery.mjs index c8e0497..f30320e 100644 --- a/scripts/lib/gallery.mjs +++ b/scripts/lib/gallery.mjs @@ -12,6 +12,7 @@ const SHELL = readTemplate('gallery.html'); const CARD = readTemplate('card.html'); const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"' }; const SCHEME_LABELS = { light: 'Light', dark: 'Dark', both: 'Light & Dark' }; +const NAME_COLLATOR = new Intl.Collator('en-US', { numeric: true, sensitivity: 'accent' }); // GitHub-style alerts (https://docs.github.com/get-started/writing-on-github). Octicon paths. const ALERTS = { @@ -223,13 +224,35 @@ function renderNav(populated) { return ``; } +function compareEntries(lhs, rhs) { + const featuredOrder = Number(rhs.featured === true) - Number(lhs.featured === true); + if (featuredOrder !== 0) { + return featuredOrder; + } + + const dateValue = (entry) => { + const value = Date.parse(entry.latest.date ?? entry.addedDate ?? ''); + return Number.isNaN(value) ? Number.NEGATIVE_INFINITY : value; + }; + + const lhsDate = dateValue(lhs); + const rhsDate = dateValue(rhs); + if (lhsDate !== rhsDate) { + return rhsDate - lhsDate; + } + + const nameOrder = NAME_COLLATOR.compare(lhs.name, rhs.name); + if (nameOrder !== 0) { + return nameOrder; + } + + return lhs.id < rhs.id ? -1 : Number(lhs.id > rhs.id); +} + export function renderGallery(index) { const groups = SECTIONS.map((section) => ({ section, - items: index.extensions.filter(section.isMatch).sort((lhs, rhs) => { - // Featured entries float to the top, then sort alphabetically - return ((rhs.featured === true) - (lhs.featured === true)) || lhs.id.localeCompare(rhs.id); - }), + items: index.extensions.filter(section.isMatch).sort(compareEntries), })); const populated = groups.filter(({ items }) => items.length > 0); diff --git a/test/gallery.test.mjs b/test/gallery.test.mjs index 192bac1..ffc2ccf 100644 --- a/test/gallery.test.mjs +++ b/test/gallery.test.mjs @@ -22,3 +22,48 @@ test('version label tooltip combines release date and notes', () => { assert.match(html, /v1\.0\.0<\/span>/); }); + +test('gallery uses the app Discover order', () => { + const entry = (id, fields = {}) => ({ + id, + name: fields.name ?? id, + description: 'Description', + author: 'Author', + homepage: 'https://example.com', + category: fields.category ?? 'extension', + addedDate: fields.addedDate, + featured: fields.featured, + latest: { + version: '1.0.0', + date: fields.releaseDate, + }, + }); + + const html = renderGallery({ + generatedAt: '2026-08-18T02:00:00Z', + extensions: [ + entry('theme', { category: 'theme', featured: true, releaseDate: '2026-08-18T02:00:00Z' }), + entry('theme-10', { name: 'Theme 10', category: 'theme' }), + entry('same-z', { name: 'Same' }), + entry('newer', { addedDate: '2026-08-10T00:00:00Z' }), + entry('featured', { featured: true, addedDate: '2026-08-01T00:00:00Z' }), + entry('theme-2', { name: 'Theme 2', category: 'theme' }), + entry('updated', { addedDate: '2026-07-01T00:00:00Z', releaseDate: '2026-08-12T00:00:00Z' }), + entry('same-a', { name: 'Same' }), + entry('older', { featured: false, addedDate: '2026-08-01T00:00:00Z' }), + ], + }); + + const ids = [...html.matchAll(/
/g)].map((match) => match[1]); + assert.deepEqual(ids, [ + 'featured', + 'updated', + 'newer', + 'older', + 'same-a', + 'same-z', + 'theme', + 'theme-2', + 'theme-10', + ]); +});